Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.45.0-wmf.8 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 Japonais/Vocabulaire/Couleurs 0 850 746028 738160 2025-07-05T18:02:16Z 2A01:CB05:8BC9:BF00:45B0:648B:122E:F9BF 746028 wikitext text/x-wiki [[Image:Books-aj.svg aj ashton 01.svg|right|70px]] {| {{tableau_japonais}} ! Couleur ! Français ! [[Japonais/Kanji|Kanji]] ! [[Japonais/Kana|Kana]] ! [[Japonais/Romaji|Rōmaji]] |- | |Couleur |[[wikt:色|色]] |いろ |iro |- |{{couleur|#CECECE}} |Argenté |[[wikt:銀色|銀色]] |ぎんいろ/シルバー |<u>gin'iro</u>/shirubā |- |{{couleur|#FFFFFF}} |Blanc |[[wikt:白い|白い]] |しろい/ホワイト |shiroi/howaito |- |{{couleur|#0000FF}} |Bleu |[[wikt:青い|青い]] <ref>Anciennement, le kanji '''青''' (ao) signifiait aussi bien « vert » que « bleu ». Aujourd'hui on emploie davantage '''緑''' (midori) pour « vert », '''青''' (ao) étant réservé pour « bleu ». Cependant, le feu vert se dit「青信号」(ao shingō).</ref> |あおい/ブルー |aoi/burū |- |{{couleur|#008080}} |Bleu canard |[[wikt:鴨の羽色|鴨の羽色]] |かものはいろ |kamo no haīro |- |{{couleur|#87CEFA}} |Bleu clair |[[wikt:水色|水色]] |みずいろ |mizuiro |- |{{couleur|#233B6C}} |Bleu marine |[[wikt:紺色|紺色]] |こんいろ |kon'iro |- |{{couleur|#00E0FF}} |Bleu turquoise |[[wikt:浅葱色|浅葱色]] |あさぎいろ |asagīro |- |{{couleur|#FF7F50}} |Corail |[[wikt:珊瑚色|珊瑚色]] |さんごいろ |san'go'iro |- |{{couleur|#DC143C}} |Cramoisi |[[wikt:紅色|紅色]] |くれないいろ |kurenaīro |- |{{couleur|#FFD700}} |Doré |[[wikt:黄金色|黄金色]] |こがねいろ/ゴールド |kogane'iro/gōrudo |- |{{couleur|#ED0000}} |Écarlate |[[wikt:緋色|緋色]] |ひいろ/スカーレット |hīro/sukāretto |- |{{couleur|#A7A7A7}} |Gris |[[wikt:鼠色|鼠色]]/[[wikt:灰色|灰色]] |ねずみいろ/はいいろ/グレー |nezumīro/haīro/gurē |- |{{couleur|#4B0082}} |Indigo |[[wikt:藍色|藍色]] |あいいろ/インジゴ |aīro/indigo |- |{{couleur|#FFFF00}} |Jaune |[[wikt:黄色|黄色]] |きいろ/イエロー |kīro/ierō |- |{{couleur|brown}} |Marron |[[wikt:茶色|茶色]] |ちゃいろ/ブラウン |cha'iro/buraun |- |{{couleur|#E0B0FF}} |Mauve |[[wikt:薄紫色|薄紫色]] |うすむらさきいろ/モーブ |usumurasakīro/mōbu |- |{{couleur|#000000}} |Noir |[[wikt:黒い|黒い]] |くろい/ブラック |kuroi/burakku |- |{{couleur|#FFA700}} |Orange |[[wikt:橙色|橙色]] |だいだいいろ/オレンジ |daidaīro/orenji |- |{{couleur|purple}} |Pourpre |[[wikt:紫|紫]] |むらさき/ぶどういろ/バイオレット |murasaki/budōiro/baioretto |- |{{couleur|#FFC0CB}} |Rose |[[wikt:桃色|桃色]] |ももいろ/ピンク |momoiro/<u>pinku</u> |- |{{couleur|#F118C6}} |Rose vif |[[wikt:撫子色|撫子色]] |なでしこいろ |nadeshiko'iro |- |{{couleur|#FF0000}} |Rouge |[[wikt:赤い|赤い]] |あかい/レッド |akai/reddo |- |{{couleur|#D71345}} |Rouge vif |[[wikt:紅色|紅色]] |べにいろ |benīro |- |{{couleur|#E34234}} |Vermillon |[[wikt:朱色|朱色]] |しゅいろ |shuiro |- |{{couleur|#008000}} |Vert |[[wikt:緑|緑]] |みどり/グリーン |midori/gurīn |- |{{couleur|plum}} |Violet |[[wikt:藤色|藤色]]/[[wikt:暗紫色|暗紫色]] |ふじいろ/あんししょく |fujīro/anshishoku |} == Exemples == * Blanc : 白人 (Hakujin) * Jaune : 黃人 (Ōjin) * Noir : 黒人 (Kokujin) L'arc-en-ciel des Super sentai comporte 16 couleurs : rouge, bleu, jaune, rose, vert, blanc, noir, orange, violet, cramoisi, bleu marine, doré, argenté, bleu ciel, gris, marron ==Références== <references /> {{Glossaires_de_Japonais}} [[Catégorie:Glossaires de Japonais]] [[Catégorie:Glossaire de couleurs|Japonais]] [[en:Japanese/Vocabulary/Colors]] [[es:Japonés/Vocabulario/Colores]] hkdiu9y1jcm67dz54gdp0cr0e5yv2j4 746076 746028 2025-07-06T07:35:37Z JackPotte 5426 Révocation d’une modification réalisée par [[Special:Contributions/2A01:CB05:8BC9:BF00:45B0:648B:122E:F9BF|2A01:CB05:8BC9:BF00:45B0:648B:122E:F9BF]] ([[User talk:2A01:CB05:8BC9:BF00:45B0:648B:122E:F9BF|discussion]]) et restauration de la dernière version réalisée par [[User:2A01:CB05:8B96:E000:CCFB:F373:69BA:56AE|2A01:CB05:8B96:E000:CCFB:F373:69BA:56AE]] 738160 wikitext text/x-wiki [[Image:Books-aj.svg aj ashton 01.svg|right|70px]] {| {{tableau_japonais}} ! Couleur ! Français ! [[Japonais/Kanji|Kanji]] ! [[Japonais/Kana|Kana]] ! [[Japonais/Romaji|Rōmaji]] |- | |Couleur |[[wikt:色|色]] |いろ |iro |- |{{couleur|#CECECE}} |Argenté |[[wikt:銀色|銀色]] |ぎんいろ/シルバー |<u>gin'iro</u>/shirubā |- |{{couleur|#FFFFFF}} |Blanc |[[wikt:白い|白い]] |しろい/ホワイト |shiroi/howaito |- |{{couleur|#0000FF}} |Bleu |[[wikt:青い|青い]] <ref>Anciennement, le kanji '''青''' (ao) signifiait aussi bien « vert » que « bleu ». Aujourd'hui on emploie davantage '''緑''' (midori) pour « vert », '''青''' (ao) étant réservé pour « bleu ». Cependant, le feu vert se dit「青信号」(ao shingō).</ref> |あおい/ブルー |aoi/burū |- |{{couleur|#008080}} |Bleu canard |[[wikt:鴨の羽色|鴨の羽色]] |かものはいろ |kamo no haīro |- |{{couleur|#87CEFA}} |Bleu clair |[[wikt:水色|水色]] |みずいろ |mizuiro |- |{{couleur|#233B6C}} |Bleu marine |[[wikt:紺色|紺色]] |こんいろ |kon'iro |- |{{couleur|#00E0FF}} |Bleu turquoise |[[wikt:浅葱色|浅葱色]] |あさぎいろ |asagīro |- |{{couleur|#FF7F50}} |Corail |[[wikt:珊瑚色|珊瑚色]] |さんごいろ |san'go'iro |- |{{couleur|#DC143C}} |Cramoisi |[[wikt:紅色|紅色]] |くれないいろ |kurenaīro |- |{{couleur|#FFD700}} |Doré |[[wikt:黄金色|黄金色]] |こがねいろ/ゴールド |kogane'iro/gōrudo |- |{{couleur|#ED0000}} |Écarlate |[[wikt:緋色|緋色]] |ひいろ/スカーレット |hīro/sukāretto |- |{{couleur|#A7A7A7}} |Gris |[[wikt:鼠色|鼠色]]/[[wikt:灰色|灰色]] |ねずみいろ/はいいろ/グレー |nezumīro/haīro/gurē |- |{{couleur|#4B0082}} |Indigo |[[wikt:藍色|藍色]] |あいいろ/インジゴ |aīro/indigo |- |{{couleur|#FFFF00}} |Jaune |[[wikt:黄色|黄色]] |きいろ/イエロー |kīro/ierō |- |{{couleur|brown}} |Marron |[[wikt:茶色|茶色]] |ちゃいろ/ブラウン |cha'iro/buraun |- |{{couleur|#E0B0FF}} |Mauve |[[wikt:薄紫色|薄紫色]] |うすむらさきいろ/モーブ |usumurasakīro/mōbu |- |{{couleur|#000000}} |Noir |[[wikt:黒い|黒い]] |くろい/ブラック |kuroi/burakku |- |{{couleur|#FFA700}} |Orange |[[wikt:橙色|橙色]] |だいだいいろ/オレンジ |daidaīro/orenji |- |{{couleur|purple}} |Pourpre |[[wikt:紫|紫]] |むらさき/ぶどういろ/バイオレット |murasaki/budōiro/baioretto |- |{{couleur|#FFC0CB}} |Rose |[[wikt:桃色|桃色]] |ももいろ/ピンク |momoiro/<u>pinku</u> |- |{{couleur|#F118C6}} |Rose vif |[[wikt:撫子色|撫子色]] |なでしこいろ |nadeshiko'iro |- |{{couleur|#FF0000}} |Rouge |[[wikt:赤い|赤い]] |あかい/レッド |akai/reddo |- |{{couleur|#D71345}} |Rouge vif |[[wikt:紅色|紅色]] |べにいろ |benīro |- |{{couleur|#E34234}} |Vermillon |[[wikt:朱色|朱色]] |しゅいろ |shuiro |- |{{couleur|#008000}} |Vert |[[wikt:緑|緑]] |みどり/グリーン |midori/gurīn |- |{{couleur|plum}} |Violet |[[wikt:藤色|藤色]]/[[wikt:暗紫色|暗紫色]] |ふじいろ/あんししょく |fujīro/anshishoku |} == Exemples == * Blanc : 白人 (Hakujin) * Jaune : 黃人 (Ōjin) * Noir : 黒人 (Kokujin) L'arc-en-ciel des Super sentai comporte 16 couleurs : rouge, bleu, jaune, rose, vert, noir, blanc, argenté, doré, orange, violet, cramoisi, bleu marine, gris, bleu ciel, marron {| {{tableau_japonais}} ! Couleur ! Français ! [[Japonais/Kanji|Kanji]] ! [[Japonais/Kana|Kana]] ! [[Japonais/Romaji|Rōmaji]] |- |{{couleur|#FF0000}} |Rouge |[[wikt:赤い|赤い]] |あかい/レッド |akai/reddo |- |{{couleur|#0000FF}} |Bleu |[[wikt:青い|青い]] |あおい/ブルー |aoi/burū |- |{{couleur|#FFFF00}} |Jaune |[[wikt:黄色|黄色]] |きいろ/イエロー |kīro/ierō |- |{{couleur|#FFC0CB}} |Rose |[[wikt:桃色|桃色]] |ももいろ/ピンク |momoiro/pinku |- |{{couleur|#008000}} |Vert |[[wikt:緑|緑]] |みどり/グリーン |midori/gurīn |- |{{couleur|#000000}} |Noir |[[wikt:黒い|黒い]] |くろい/ブラック |kuroi/burakku |- |{{couleur|#FFFFFF}} |Blanc |[[wikt:白い|白い]] |しろい/ホワイト |shiroi/howaito |- |{{couleur|#CECECE}} |Argenté |[[wikt:銀色|銀色]] |ぎんいろ/シルバー |gin'iro/shirubā |- |{{couleur|#FFD700}} |Doré |[[wikt:黄金色|黄金色]] |こがねいろ/ゴールド |kogane'iro/gōrudo |- |{{couleur|#FFA700}} |Orange |[[wikt:橙色|橙色]] |だいだいいろ/オレンジ |daidaīro/orenji |- |{{couleur|purple}} |Violet |[[wikt:紫|紫]] |むらさき/ぶどういろ/バイオレット |murasaki/budōiro/baioretto |- |{{couleur|#DC143C}} |Cramoisi |[[wikt:紅色|紅色]] |くれないいろ |kurenaīro |- |{{couleur|#233B6C}} |Bleu marine |[[wikt:紺色|紺色]] |こんいろ |kon'iro |- |{{couleur|#A7A7A7}} |Gris |[[wikt:鼠色|鼠色]]/[[wikt:灰色|灰色]] |ねずみいろ/はいいろ/グレー |nezumīro/haīro/gurē |- |{{couleur|#87CEFA}} |Bleu clair |[[wikt:水色|水色]] |みずいろ |mizuiro |- |{{couleur|brown}} |Marron |[[wikt:茶色|茶色]] |ちゃいろ/ブラウン |cha'iro/buraun |- |} ==Références== <references /> {{Glossaires_de_Japonais}} [[Catégorie:Glossaires de Japonais]] [[Catégorie:Glossaire de couleurs|Japonais]] [[en:Japanese/Vocabulary/Colors]] [[es:Japonés/Vocabulario/Colores]] 7j139lzazag9lzo0d8awbkcx9tpi6by Astrologie/Les différents facteurs astrologiques 0 54639 745963 745955 2025-07-05T12:00:13Z Kad'Astres 30330 745963 wikitext text/x-wiki #[[/Les Planètes/]] : Si l'on compare la vie à un film, ce sont les Planètes, et non les Signes, qui en sont les Acteurs. #[[/Les Signes/]] #[[/Les Maisons/]] #[[/Les Aspects/]] #[[/Les trois Croix et les quatre Éléments/]] #[[/Autres facteurs d'Interprétation/]] [[Catégorie:Astrologie]] 59is63vtvmdwzkvyqy1zsorevnw98kv Fonctionnement d'un ordinateur/Le renommage de registres 0 65844 746008 741560 2025-07-05T15:10:54Z Mewtow 31375 /* Le renommage de registre total et partiel */ 746008 wikitext text/x-wiki Pour améliorer l'exécution dans le désordre, les concepteurs de processeurs ont étudié des méthodes pour éliminer la grosse majorité des dépendances de données lors de l’exécution. Dans ce que j'ai dit précédemment, j'ai évoqué trois types de dépendances de données. Les dépendances RAW (''Read After Write'') sont des dépendances contre lesquelles on ne peut rien faire : ce sont de « vraies » dépendances de données. Elles imposent un certain ordre d’exécution de nos instructions. Mais les dépendances WAW (''write after write'') et WAR (''write after read'') sont de fausses dépendances, nées du fait qu'un emplacement mémoire est réutilisé, pour stocker des données différentes à des instants différents. Bien évidemment, l’imagination débridée des concepteurs de processeur a trouvé une solution pour supprimer ces dépendances : le '''renommage de registres'''. Avant toute chose, précisons que ce chapitre abordera uniquement les techniques compatibles avec les exceptions précises. En clair, seulement les techniques utilisées sur les processeurs modernes. Il existe deux autres techniques de renommage de registre utilisées sur les processeurs sans exceptions précises : l'algorithme de Tomasulo et le ''scoreboarding''. Contrairement à ce que vous avez pu lire ailleurs, il y a une forme de renommage de registre dans le ''scoreboarding'', mais nous verrons cela dans quelques chapitres. Toujours est-il que ces deux techniques historiques étaient utilisées sur d'anciens ordinateurs, qui n'implémentaient pas la prédiction de branchement. Leurs techniques de renommage de registres étaient particulières, aussi nous les verrons dans un chapitre à part. ==Le renommage de registres : généralités== Les dépendances WAR et WAW sont souvent opposées aux dépendances vraies (true dependency), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Un même nom de registre identifie une donnée différente à des instants différents du programme, ce qui force à exécuter les isntructions qui utilisent ce registre dans l'ordre. D'où le fait que les dépendances WAR et WAW sont souvent appelées des dépendances de nom (naming dependency). Si on change l'ordre de deux instructions ayant une dépendance de données WAW ou WAR, on peut se retrouver dans une situation où les deux instructions veulent stocker des données différentes en même temps dans le même registre, ce qui n'est pas possible. Mais elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Un même nom de registre correspond alors à une donnée. Une solution simple vient alors à l'esprit : conserver chaque donnée dans son propre registre et choisir le registre adéquat à l'utilisation. Un registre architectural correspond alors à plusieurs registres, chacun contenant une donnée bien précise. L'idée est qu'au lieu d'avoir un registre qui contient des données différentes à des instants différents, on a autant de registres que de données, ce qui améliore les possibilités de réorganisation des instructions. Il faut ainsi faire la distinction entre : * '''registres architecturaux''', définis par le jeu d'instructions ; * '''registres physiques''', physiquement présents dans l'ordinateur. * '''registres virtuels''' utilisés pour le renommage de registre mais invisibles pour le programmeur. Les registres physiques regroupent l'ensemble des registres, à savoir les registres architecturaux et virtuels. Pour illustrer l'idée, prenons l'exemple suivant, où un registre architectural nommé R6 est utilisé pour stocker trois données consécutives. Il mémorise d'abord le nombre 255, puis le nombre 2567763, puis zéro. On suppose que ces trois valeurs sont indépendantes, dans le sens où elles sont utilisées par des instructions sans dépendances RAW. En théorie, on pourrait exécuter ces instructions séparément, mais ce n'est pas possible. Le fait est que l'on doit d'abord faire les calculs avec l'opérande 255, puis ceux avec l'opérande 2567763, puis ceux avec l'opérande zéro. La raison est que pour que le calcul démarre, il faut que l'opérande soit disponible. Et l’usage d'un seul registre pour stocker des opérandes différentes à des temps différents limite les possibilités d’exécution en parallèle. Avec le renommage de registres, ces trois données sont enregistrées dans des registres physiques séparés, dès qu'elles sont disponibles. Le processeur garde évidemment trace du fait que ces trois registres physiques correspondent au registre architectural R6. Si les trois valeurs n'ont pas de dépendances entre elles, les instructions liées peuvent être exécutées dans le désordre et s’exécuter en même temps. Ce qui est le cas dans le schéma suivant, où certaines instructions sont exécutées en avance, ce qui réserve les registres physiques en avance. [[File:Renommage de registres - principe.png|centre|vignette|upright=2|Exemple où un registre architectural est utilisé pour contenir trois données successives.]] : Pour les connaisseurs, le renommage de registres réécrit le programme exécuté dans une représentation appelée SSA, utilisée par les compilateurs lors de la compilation. Et cette réécriture se fait un peu de la même manière avec un compilateur. Là où le compilateur change le nom des variables pour passer en forme SSA, le processeur change les noms de registres pour obtenir de l'assembleur SSA interne au processeur. ===La durée de vie des registres virtuels=== La plupart des processeurs disposent d'un banc de registre physique unique, qui regroupe registres physiques et virtuels. Mais il existe quelques processeurs qui font autrement. Ils ont un banc de registre pour les registres architecturaux, et les registres virtuels sont placés ailleurs dans le processeur, typiquement dans le tampon de ré-ordonnancement (ROB) ou la fenêtre d'instruction. Pour simplifier, considérons qu'ils sont dans un banc de registre spécialisé qui regroupe les registres virtuels. C'est un peu faux en soi, mais faisons comme si. Dans le reste de cette section, nous allons partir du principe que les registres virtuels et architecturaux sont séparés pour simplifier les explications, nous verrons les autres formes de renommage de registre plus tard. Les registres virtuels ont une certaine durée de vie. On veut dire par là qu'une donnée écrite dans un registre virtuel y reste durant un certain temps, mais pas indéfiniment. Un registre virtuel est utilisé par une instruction en cours d'exécution dans le pipeline. Il nait lors du renommage de registre et meurt une fois l'instruction terminée. En clair, un registre virtuel dure durant toutes les étapes du pipeline qui suivent le renommage de registres. Une fois que l'instruction quitte le pipeline, qu'elle sort du ROB, le registre virtuel est inutile et doit laisser la place au registre architectural non-renommé. Le résultat de l'instruction doit être transféré dans le registre architectural adéquat, le registre de destination de l'instruction. Si les registres virtuels et architecturaux sont séparés, il faut copier la donnée des registres virtuels vers les registres architecturaux. Utiliser un banc de registre unique résout le problème, mais n'allons pas trop vite, nous verrons cela en détail dans la suite. Toujours est-il qu'un registre virtuel est libéré quand l'instruction associée quitte le ROB. Il est théoriquement possible de libérer un registre virtuel en avance, si on est certain qu'aucune instruction n'ira lire ce registre. Pour cela, le mieux est d'attribuer un compteur de lectures pour chaque registre. À noter que pour maintenir des exceptions précises, on est obligé d'attendre que la dernière instruction qui lit le registre ait validé. Le jeu n'en vaut clairement pas la chandelle. Le seul intérêt d'une telle optimisation est économiser des registres virtuels, d'avoir un banc de registres virtuels plus petits, mais cela se fait au prix de l'ajout de circuits qui consomment tout autant, si ce n'est plus de circuits, pour un résultat en termes de performances limité, voire nul. Après avoir vu la mort des registres virtuels, voyons leur naissance. Le renommage de registre a lieu après le décodage, donc juste avant le passage dans le ''scoreboard'', la fenêtre d’instruction, la file d'instruction ou toute autre structure dédiée à l'émission. En clair : les registres virtuels sont attribués juste avant émission et ne sont utiles qu'après. Les instructions susceptibles d'utiliser un registre virtuel sont donc les instructions émises ou prêtes à l'être, mais pas encore terminées. Les instructions en question sont soit en attente dans la fenêtre d’instruction, soit en cours d'exécution dans le chemin de données, soit en attente dans le ROB. Ces dernières attendent que les instructions précédentes se terminent. Il faut noter que toutes les instructions émises ou en passe de l'être ont été ajoutées dans le ROB juste après le renommage. Même les instructions en attente dans la fenêtre d'instruction sont dans le ROB. Pour résumer, le ROB garde la trace de ces instructions prêtes à être émises ou déjà émises. ===Le nombre idéal de registres virtuels=== Mine de rien, le contenu de la section précédente nous donne une seconde borne maximale sur le nombre de registres virtuels utiles. Supposons que chaque instruction fournit un résultat, stocké dans un registre virtuel rien que pour lui. Le nombre maximal d'instructions chargées dans le pipeline après émission est égal au nombre d'entrée dans le ROB. Il peut y avoir moins d'instructions, ce qui fait que des entrées du ROB sont inutilisées, mais supposons que nous soyons dans le meilleur des cas. Dans ce cas, on doit avoir un registre virtuel par instruction, ce qui fait au maximum un registre virtuel par entrée du ROB. Rien ne sert d'avoir plus de registres virtuels que d'entrées dans le ROB, il n'y aura pas assez d’instructions dans le pipeline pour tous les utiliser. Après avoir vu une borne maximale, voyons une limite minimale. Non pas qu'il faille absolument plus de registres virtuels que cette limite, simplement qu'il est préférable d'en avoir que cette limite. Partons du principe que les instructions pouvant utiliser un registre virtuel sont celles qui sont soit déjà émises, soit en attente de l'être. Si on néglige les instructions en attente dans le ROB, alors les instructions émises sont dans les ALUs. Le nombre d'instruction est donc égal à la somme de : la taille de la fenêtre d'instruction (instructions en attente), du nombre d'unités de calcul (instruction en cours d'exécution), unités d'accès mémoire inclues. Mieux vaut ne pas passer sous la somme taille de la fenêtre d'instruction + nombre d'unités de calcul. Un bon compromis est d'avoir un nombre de registre virtuel compris entre ces deux bornes. Le nombre réel de registre virtuels est très souvent inférieur au nombre d'entrées du ROB, car toutes les instructions ne fournissent pas de résultat à enregistrer dans les registres. C'est le cas pour les instructions de branchement, par exemple, ou les écritures en mémoire RAM. Par contre, il vaut mieux avoir plus de registres que d'entrées dans la fenêtre d'instructions. La majorité es processeurs respectent ces deux limites basse et hautes, avec un nombre de registres réels entre les deux. Les quelques rares exceptions étaient des processeurs qui implémentaient le renommage de registre pour la première fois, et où ce genre de détails n'étaient pas bien compris. Par exemple, on peut citer le PowerPC 604, qui avait 20 registres virtuels, alors que le ROB avait seulement 16 entrées. Les 4 registres virtuels en trop n'étaient pas utilisés. Le processeur qui a immédiatement suivi, le PowerPC 604, n'a pas fait cette erreur et a réduit le nombre de registres virtuels à seulement 16. Un autre exemple est le MIPS R10000, avec son ROB à 32 entrées, sa fenêtre d'instruction à 48 entrées et ses 64 registres virtuels. Le processeur suivant, le R12000, corrigea partiellement le problème en augmentant le ROB à 48 entrées, ce qui était insuffisant, mais bienvenu. ===Le renommage de registre total et partiel=== De nombreux processeurs ont des registres architecturaux séparés pour les nombres flottants et entiers. Dans ce cas, leur renommage est indépendant : on peut renommer les registres flottants à part des registres entiers. Mais les deux types de registres sont renommés à part. A ce propos, quelques rares processeurs ne renomment que que les registres flottants, ou que les registres entiers ! Pour donner un exemple, le processeur Nx586 ne renommait que les registres entiers, pour les instructions entières. Un autre exemple est celui de l'IBM System/360 Model 91, sur lequel les registres flottants étaient renommés, mais pas les registres entiers. La même chose était présente sur anciens processeurs x86 sur lesquels le renommage de registre a été introduit, [[File:Zen microarchitecture.svg|centre|vignette|upright=2|Microarchitecture Zen 1 des CPU d'AMD.]] Le renommage de registre peut être total ou partiel. Avec le renommage total, toutes les instructions voient leurs registres renommés. Il s'agit de la méthode la plus utilisée à ce jour, par simplicité de conception. Mais des processeurs assez anciens utilisaient un renommage partiel, qui ne s’appliquaient qu'à certains types d'instruction bien précis. Par exemple, on pourrait citer les processeurs Power1, Power2 et PowerPC 601. Sur le premier, seuls les registres flottants étaient renommés, et seulement pour les instructions de lecture flottante. La raison est que le processeur n'a qu'une seule FPU et ne peut donc pas exécuter des opérations flottantes dans le désordre sur celle-ci, ce qui fait que renommer les registres ne sert à rien. Le processeur Power 2 dispose lui de plusieurs FPU et le renommage de registres touche aussi les instructions flottantes arithmétiques, c'est un renommage total. Les processeur qui utilisaient le renommage partiel étaient des processeurs assez anciens, qui faisaient avec un budget en transistors limité. De plus, la technique n'était pas très bien maitrisée, et était encore balbutiante. La technique a pourtant été proposée dès 1970 dans divers articles académiques, et raffinée pendant les années 70. Mais le budget en transistors du renommage de registre a fait qu'elle n'a pas été utilisée avant que la loi de Moore fasse son œuvre. ==Les différentes implémentations du renommage de registres== Dans l'implémentation la plus simple du renommage de registres, tous les registres physiques sont stockés dans un seul gros banc de registres, qui regroupe registres virtuels et architecturaux. Mais d'autres implémentations font totalement autrement, et séparent les registres virtuels et registres architecturaux. Voyons maintenant les différentes implémentations possibles du renommage de registres. Mais avant toute chose, parlons du réseau de contournement. Pour simplifier les explications, appelons ''instruction productrice'' l'instruction qui fournit un résultat, et instruction consommatrice celle qui l'utilise comme opérande. Si une instruction consommatrice est chargée dans le pipeline quelques cycles après l'instruction productrice, les deux sont dans le pipeline en même temps. Dans ce cas, on pourrait penser que c'est le réseau de contournement qui gérerait le transfert du résultat de l'instruction productrice à l’instruction consommatrice. Et effectivement, c'est souvent le cas. Mais les registres virtuels sont pris en compte dans le système de contournement. Si le résultat est écrit dans un registre virtuel, il peut aussi être lu dans ce registre virtuel. Lire un résultat dans les registres virtuels permet d'implémenter une forme de contournement. Tout cela pour dire qu'il peut y avoir un lien entre contournement et renommage de registres, les deux doivent être conçu de manière à communiquer entre eux. Typiquement, il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres. Ce dernier gère le cas où un résultat d’instruction est réutilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le système de renommage de registres qui s'en charge. ===Le renommage à banc de registres physiques=== Les processeurs modernes utilisent un seul banc de registres pour les registres architecturaux et virtuels, appelé '''banc de registres physiques''' (''physical register file''). Le tampon de réordonnancement et la fenêtre d'instruction contiennent des numéros de registres virtuels, des pointeurs vers les registres. Il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres, qui gère le cas où la sortie de l'ALU est utilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le banc de registres physiques qui s'en charge. Les opérandes viennent donc soit du banc de registres physique, soit du réseau de contournement. [[File:Renommage à banc de registres physiques.png|centre|vignette|upright=2|Renommage à banc de registres physiques.]] Avec ce système, un registre physique peut être dans quatre états. * Le premier état est celui d'un ''registre disponible'', près à servir de registre de destination. Par disponible, on veut dire que ce registre peut être réservé par l'unité de renommage, mais qu'il n'a pas encore été réservé. Ce n'est ni un registre virtuel, ni un registre architectural, juste un registre inutilisé. * Le second état est l'''état réservé'', celui d'un registre virtuel qui attend qu'une instruction écrive son résultat dedans. L'état réservé réserve un registre en écriture, mais interdit les lectures dedans. * Le troisième état est celui d'un registre virtuel dans lequel une instruction a écrit son résultat, mais l'instruction n'a pas encore quitté le ROB. Le registre est alors appelé un ''registre écrit''. * Enfin, le quatrième est celui d'un ''registre architectural'', qui contient une donnée valide, dans le sens où il n'est associé à aucune instruction dans le ROB. Un registre passe normalement par ces quatre états dans l'ordre disponible, virtuel réservé, virtuel écrit, architectural. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel devient un registre architectural. Cependant, cela suppose que l'instruction quitte le ROB normalement, ce qui n'est pas le cas suite à une mauvaise prédiction de branchement ou autre. Dans ce cas, il repasse immédiatement à l'état disponible. L'unité de renommage des registre mémorise l'état de chaque registre pour savoir dans quel état il est. Avec ce système, les données ne sont pas déplacées, ce qui a un avantage en termes de performance, de consommation énergétique et de simplification des circuits du processeur. Une fois enregistrées dans un registre, les données ne se déplacent plus. Avec un banc de registre virtuel séparé, elles sont recopiées d'un banc de registre virtuel vers un banc de registre architectural. Et les autres techniques que nous allons voir ont le même problème. Un autre avantage est que les opérandes proviennent toutes du même endroit : le banc de registre unique. Avec un banc de registre séparés, les opérandes peuvent provenir soit des registres architecturaux, soit des registres virtuels. Les opérandes peuvent donc provenir du banc de registre architectural, mais aussi du banc de registre renommés. La conséquence est que la gestion des interconnexions est beaucoup plus compliquée, notamment quand on veut implémenter le contournement. Pas de problèmes de ce genre avec un banc de registre unique. ===Le renommage dans le tampon de réordonnancement=== Il est possible de faire le renommage dans le tampon de réordonnancement (ROB, pour ''ReOrder Buffer'')). Cela veut dire que les résultats des instructions sont mémorisés dans le tampon de réordonnancement. Les entrées du ROB se voient ajouter un champ pour mémoriser le résultat d'une instruction, et ce champ sert de registre virtuel. Les entrées du ROB deviennent ainsi adressables, elle ont un numéro, qui sert de numéro/nom de registre virtuel. Avec cette technique, les registres architecturaux sont séparés des registres virtuels inclus dans le ROB. Les registres architecturaux sont regroupés dans un banc de registres spécialisé, appelé le '''''retirement register file'''''. La fenêtre d'instruction mémorise soit un nom de registre architectural, soit l'adresse de l'entrée du ROB qui contient le résultat voulu. Quand une instruction a tous ses opérandes de prêts, ceux-ci sont lus depuis le ROB ou les registres architecturaux. Pour cela, le ROB mémorise l'état de chaque entrée, de chaque registre virtuel : vide, réservée pour un résultat, écrite avec un résultat. [[File:Renommage dans le tampon de réordonnancement.png|centre|vignette|upright=2|Renommage dans le tampon de réordonnancement.]] Un défaut est que cela complexifie l'implémentation des interconnexions par rapport à un banc de registre physique. Il y a toujours un réseau de contournement pour réutiliser un résultat un cycle plus tard. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le ROB qui s'en charge. Les opérandes peuvent être lues depuis trois sources : le ROB, le banc de registre architectural, et le réseau de contournement. Soit une source de plus qu'avec la technique précédente, ce qui complexifie les multiplexeurs de contournement. Et cela a un effet sur la latence des opérations, le temps de traversée de ces multiplexeurs n'est pas gratuit, sans compter le temps mis pour les signaux de commande pour les configurer. Le renommage de registre est aussi plus compliqué. Au lieu de simplement remplacer un numéro de registre architectural par un numéro de registre physique, on doit faire la différence entre numéro de registre et adresse dans le ROB, pour savoir où lire les opérandes. Sans quoi ne sait pas configurer les multiplexeurs et choisir la bonne source pour les opérandes. Un autre défaut est que les registres virtuels doivent être copiés dans les registres architecturaux quand une instruction quitte le ROB. La copie se fait ici du ROB vers le banc de registre architectural. Et copier la donnée a un cout en énergie que l'usage d'un banc de registre unique n'a pas. Un dernier défaut est que le nombre de registres virtuel est égal au nombre d'entrées dans le ROB. Or, on a vu plus haut que c'est un peu du gâchis, car beaucoup d'instructions ne produisent aucun résultat. Le hardware ajouté pour les entrées est donc sous-utilisé, il ne sert que si l'instruction fournit un résultat, mais ne sert à rien sinon. ===Le renommage avec un tampon de renommage=== Le dernier défaut évoqué dit que beaucoup d'entrées du ROB sont inutilisées et que les registres virtuels associés sont gâchés. Pour éviter cela, il est possible de sortir les résultats d'instruction du ROB et de les placer dans une mémoire FIFO complémentaire du ROB, appelée le '''tampon de renommage'''. Pour le dire autrement, le ROB amélioré est scindé en deux, avec le ROB proprement dit d'un côté, et un pseudo-ROB pour le renommage de registres et le contournement. Pour information, cette technique de renommage était utilisée dans d'anciens processeurs commerciaux, comme les Pentium Pro, le Pentium II, ou encore le Pentium III, ainsi que dans le Core 2 Duo. La technique est souvent expliquée en décrivant le tampon de renommage comme un banc de registre séparé pour les registres virtuels, appelé le '''banc de registres renommés''' (''rename register file''). Il est séparé du banc de registres architecturaux, avec lequel il communique. La raison à cela est que le tampon de renommage est adressable alors que le ROB ne l'est pas. Il faut dire que le tampon de renommage a tout d'un banc de registre : il contient les registres virtuels et seulement ceux-ci, on peut l'adresser, lire des opérandes dedans, écrire des résultats dedans, etc. Par contre, il y a une différence avec un banc de registre normal : il est traité comme une mémoire FIFO. Nous avons déjà vu comment implémenter une FIFO avec un banc de registre et quelques registres, aussi je ne fais pas de rappels sur ce point. S'il s'agissait d'un véritable banc de registre isolé, sans rien de plus, le ROB et la fenêtre d'instruction contiendraient des numéros de registres virtuels, qui adressent directement le banc de registre renommé. Une telle implémentation est possible, mais aucun processeur commercial n'a utilisé une telle implémentation. A la place, le banc de registre est en réalité une mémoire FIFO, dont les entrées sont adressables, avec de plus un système de cache utilisé pour faire la correspondance entre nom de registre architectural et nom de registre virtuel. Nous détaillerons ce dernier point dans la section qui explique comment fonctionne l'unité de renommage. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel est libéré, à savoir qu'il repasse en état disponible. C'est à ce moment qu'à lieu la copie de la donnée dans le registre architectural adéquat. Il faut noter que le registre virtuel n'est pas effacé, il contient toujours l'ancienne donnée. Mais ce n'est pas un problème. S'il est réservé par une instruction, elle écrira dedans, ce qui écrasera cette donnée. Et de manière générale, il est impossible de lire cette donnée, que ce soit si le registre est disponible ou réservé. [[File:Renommage à banc de registres renommés.png|centre|vignette|upright=2|Renommage à banc de registres renommés.]] La technique partage tous les défauts du renommage dans le ROB. Le réseau de contournement est plus compliqué car les opérandes peuvent venir de trois sources : les deux bancs de registres et le réseau de contournement. De plus, les registres virtuels doivent être copiés dans les registres architecturaux, avec tout ce que cela implique. Déjà, déplacer des données implique un cout en énergie et en consommation d'électricité, qu'il vaut mieux éviter. De plus, il faut ajouter un port de lecture sur le banc de registre renommés, pour copier un registre sans nuire aux performances. Le banc de registres physiques n'a pas à être modifié : le port d'écriture existant suffit pour la copie, vu qu'on n'écrit dedans que pour copier des résultats d'instructions validées/terminées. Par contre, le cout en transistors est plus faible qu'avec le renommage dans le ROB, vu que le banc de registre a moins de registres virtuels que le ROB. La technique n'impose pas d'avoir autant de registres virtuels que d'entrées dans le ROB, ce qui permet d'en réduire le nombre. Par contre, comparé à l'usage d'un banc de registres unique, le cout en transistors est plus élevé. La différence se fait surtout au niveau des ports de lecture/écriture du banc de registres. La raison est que l'on doit ajouter un port de lecture pour copier les résultats dans les registres architecturaux. Du moins, dans le cas le plus simple. C'est le cas sur un processeur qui permet de compléter l'exécution d'une instruction par cycle. Mais certains processeurs peuvent terminer l'exécution de plusieurs instructions par cycle. C'est notamment le cas des processeurs dits superscalaires, qui sont capables de charger, exécuter et terminer plusieurs instructions par cycle. Ils auront droit à leur chapitre dédié, mais passons. Toujours est-il que si plusieurs instructions peuvent se terminer en même temps, il faut ajouter autant de ports de lecture sur le banc de registre pour copier leurs résultats. ===Le renommage physique-virtuel=== Les méthodes mentionnées au-dessus ont un léger problème : beaucoup de registres physiques sont gâchés, car attribués à une instruction dès l'étage de renommage et libérés lorsque on est certain qu'aucune instruction ne lira le registre physique attribué. Et parfois, les registres sont alloués trop tôt, alors qu'ils auraient pu rester libres durant encore quelques cycles. C'est notamment le cas quand l'instruction renommée attend un opérande dans la fenêtre d'instruction, ou quand le résultat est en cours de calcul dans l'unité de calcul. Ces situations ont toutes la même origine : le renommage a lieu tôt dans le pipeline, pour garder les dépendances entre instructions. Mais dans les faits, rien n'oblige à utiliser les registres architecturaux pour conserver les dépendances. On peut tout simplement attribuer un tag à chaque résultat d'instruction, tag qui ne correspond pas à un registre architectural. Et ce tag sera alloué à un registre architectural le plus tard possible, quand l'instruction fournira son résultat. Ce genre de méthodes a été formalisée avec ce qu'on appelle la technique du '''banc de registres physiques-virtuels''' (physical-virtual register file). Cette méthode demande d'ajouter une seconde table de correspondance, qui fait le lien entre le tag et le registre physique. De plus, la table d’alias de registres doit être modifiée : elle ne doit pas seulement faire la correspondance entre le nom de registre et le tag, mais aussi avec le registre physique s'il est déjà attribué. Ainsi, lors du renommage en sortie du décodeur, on peut renommer l'instruction avec les registres physiques si ceux-ci sont connus lors du renommage, et renommer avec des tags dans le cas contraire. Il faut noter que cette méthode a un léger problème : quand une instruction termine et que le résultat doit se voir attribuer un tag, il se peut parfaitement qu'il n'y ait plus de registre physique de libre. Les solutions pour régler ce problème sont assez complexes, aussi je n'en parlerai pas ici. ==L’unité de renommage== Pour commencer, sachez qu'il existe deux méthodes pour implémenter l'unité de renommage. La première utilise une unité à part, séparées, qui contient de nombreuses mémoires caches/FIFO pour stocker des informations nécessaires pour le renommage de registres. La seconde mémorise les informations de renommage dans le ROB ou le tampon de renommage, voir ailleurs. Les deux types d'unités de renommage sont assez différentes. Pour les désigner, je vais parler d''''implémentation par ''tag''''' et d''''implémentation associative'''. De plus, les deux types d'unités ne sont pas utilisées sur les mêmes implémentations. Par exemple, avec un banc de registre physique, il est naturel d'utiliser une implémentation par ''tag'', l'autre implémentation n'est pas pratique. Aussi, tous les processeurs avec un banc de registre physique utilisent une unité de renommage pat ''tag''. Mais pour le renommage dans le ROB, c'est l'inverse : les deux implémentations sont possibles, mais il est préférable d'utiliser une implémentation associative. Pour simplifier les explications, je vais commencer par décrire le premier type, avec un banc de registre physique. {|class="wikitable" |- ! ! Implémentation par ''tag'' ! Implémentation associative |- ! Banc de registre physique | Seule implémentation possible | |- ! Renommage dans le ROB | Possible, certains processeurs utilisent la technique | Possible, certains processeurs utilisent la technique |- ! Renommage dans un tampon de renommage | Possible, tous les processeurs utilisent la technique | Possible, aucun processeur connu |} ===Les unités de renommage par ''tag''=== Les registres architecturaux sont identifiés par des noms de registre, tout comme les registres physiques. Les noms de registre des registres physiques sont appelés des '''tags'''. Les registres physiques sont bien plus nombreux que les registres architecturaux, sans quoi le renommage de registre n'a aucun intérêt. La conséquence est que les numéros de registres physiques sont plus grands que les numéros de registres architecturaux. Le renommage de registres remplace les noms/numéros de registres architecturaux par des numéros de registres physiques, par des ''tags''. D'où le terme de ''renommage'' de registre. Ce remplacement est effectué dans un étage supplémentaire du pipeline, intercalé entre le décodage et l'émission. [[File:Renommage de registres.png|centre|vignette|upright=2|Renommage de registres.]] Le remplacement se fait cependant différemment pour le registre de destination et les registres d'opérandes. Le registre de destination est associé à un registre virtuel disponible. L'allocation du registre de destination prend un registre disponible et le place en état réservé. Une nouvelle correspondance entre registre architectural et virtuel est alors crée. Mais pour les opérandes, c'est autre chose. Les opérandes sont soit dans un registre architectural, soit dans un registre virtuel en état écrit. Les noms de registres architecturaux pour les opérandes doivent être remplacé par le nom de registre adéquat, déjà attribué. Une sacrée différence : on attribue un registre virtuel pour le résultat, les opérandes ont déjà un registre virtuel attribué. Un autre point est que l'unité de renommage ne fait pas que modifier les registres opérande/de destination. Elle gère aussi la disponibilité des opérandes. Plus haut, on a dit qu'un registre virtuel pouvait être dans quatre états. Soit il est disponible, soit il est réservé, soit il est écrit, soit la donnée est dans un registre architectural. L'unité de renommage est au courant de l'état de chaque registre. Les quatre états possibles sont utilisés pour diverses raisons. Et il y a deux manières pour mémoriser la correspondance entre un registre physique et un registre architectural. La première est d'utiliser un circuit à part, qui fait partie de l'unité de renommage, l'autre est d'intégrer les correspondances dans le ROB ou toute autre structure dédiée. Pr exemple, on peut ajouter la correspondance dans le ROB, dans la fenêtre d'instruction, éventuellement dans le banc de registres renommés ! Dans ce qui suit, nous allons parler du cas où on utilise une mémoire à part, partie intégrante de l'unité de renommage de registre. Nous verrons l'intégration dans le ROB après, de même que la relation avec les quatre techniques précédentes. ===Renommer le registre de destination : la liste de disponibilité=== Renommer le registre de destination de l'instruction demande juste de de lister tous les registres disponibles. Pour renommer un registre de destination, il suffit de lui attribuer un registre virtuel inutilisé. Pour cela, certaines techniques de renommage de registres mémorisent la liste des registres disponibles dans une petite mémoire : la '''liste de disponibilités''' (''free list''). L'implémentation de la liste de disponibilité varie grandement d'un processeur à l'autre. Avec certaines méthodes de renommage de registre, qui impliquent un ROB, on peut même s'en passer totalement. Dans sa version la plus simple, elle contient un bit pour chaque registre, qui indique s'il est disponible ou non. Le bit en question est mis à jour quand une instruction entre/quitte le tampon de réordonnancement avec ce registre comme opérande. Lorsqu'un registre est attribué, il quitte la liste de disponibilités. Lorsque son instruction quitte le ROB, le bit de disponibilité est mis à jour. Une version plus évoluée utilise une mémoire FIFO qui contient des noms de registres. Le renommage du registre de destination utilise cette FIFO pour sortir le numéro de registre virtuel adéquat. Dans la suite, nous partirons du principe que c'est cette méthode qui est utilisée. Il faut noter que si le renommage est fait dans le ROB, la liste de disponibilité est inutile. La raison est que le ROB est déjà une mémoire FIFO, comme la liste de disponibilité. De plus, les registres virtuels sont dans le ROB. Attribuer un registre virtuel consiste simplement à mobiliser une entrée vide du ROB, en enfilant l'instruction dedans. Le ROB sait quelle est la prochaine entrée à allouer à une instruction émise, du fait de son caractère FIFO. ===La table de correspondances de registres=== Le renommage du registre de destination attribue un registre virtuel pour un résultat d'instruction. Reste qu'il faut maintenir cette attribution tant que l'instruction ne s'est pas terminée. Pendant toute la durée de vie de l'instruction, il faut se souvenir que tel registre virtuel correspond à tel résultat d'instruction, mais aussi à tel registre architectural. En effet, les instructions suivantes vont lire ce résultat pour l'utiliser comme opérande. Elle encoderont ce résultat/opérande avec le registre architectural censé contenir l'opérande. Le renommage de registre devra alors remplacer les registres opérande par le registre virtuel adéquat, qui contient l'opérande. Le renommage des registres opérande est le rôle de la '''table d’alias de registres''' (''register alias table''), une mémoire qui contient le registre virtuel associé à chaque registre architectural. Elle mémorise précisément une table de correspondance entre registre architectural et registre virtuel/physique. La table d'alias contient une correspondance par registre architectural, qui change au gré du renommage des instruction. De plus, elle contient un bit qui indique si le registre a été renommé ou non. S'il n'a pas été renommé, alors l'opérande est disponible dans les registres architecturaux, pas dans un registre virtuel. C'est très utile sur les processeurs avec un banc de registre séparés pour les registres architecturaux. Cela permet de savoir où lire les opérandes. La correspondance en question est mise à jour quand une instruction est émise, et quand elle termine. Premièrement, elle est mise à jour à chaque fois qu'un registre de destination est renommé. En clair, la table d’alias de registres est mise à jour à chaque lecture dans la table de disponibilité. Le registre virtuel choisi dans la liste de disponibilités est attribué au registre architectural de destination du résultat. Il suffira de réutiliser cette correspondance par la suite. Deuxièmement, elle est mise à jour quand une instruction termine, quand elle quitte le ROB. Le ROB envoie alors le numéro du registre virtuel qui contenait le résultat enregistré dans les registres architecturaux. Le numéro est alors enregistré dans la table de disponibilité. De plus, la correspondance associée dans la table d'alias est effacée. [[File:Renommage de registres - circuits.png|centre|vignette|upright=2|Unité de renommage de registres.]] Il existe deux façons pour implémenter la table d'alias. La plus ancienne consiste à utiliser une mémoire associative dont le tag contient le nom du registre architectural et la donnée le nom du registre physique. Sur les processeurs plus récents, on utilise une mémoire RAM, dont les adresses correspondent aux noms de registres architecturaux, et dont le contenu d'une adresse correspond au nom du registre physique associé. On n'a alors qu'une seule correspondance entre registre physique et registre architectural, mais cela ne pose pas de problème si on renomme les instructions dans l'ordre d'émission (ce qui est toujours fait). Notons que la table l'alias doit avoir deux ports de lecture : un pour chaque opérande. Et cela vaut peu importe son implémentation : le cache comme la RAM doivent avoir deux ports. ==L'interaction entre renommage de registres et autres optimisations== L'unité de renommage de registre ne fait pas que du renommage de registre, mais joue aussi un rôle dans l'exécution dans le désordre, et prend en charge des rôles annexes à son rôle principal. Par exemple, elle est impliquée pour corriger les mauvaises prédiction de branchement, les exceptions matérielle ou toute situation qui demande de vider le pipeline. En dehors de ces situations, un registre passe normalement par ces quatre état dans l'ordre disponible, réservé, écrit, architectural. Mais ce n'est pas le cas lors d'une mauvaise prédiction de branchement ou autre. Dans ce cas, tous les registres passent immédiatement à l'état disponible, sauf les registres architecturaux. ===La récupération après une prédiction invalide=== Quand une exception ou une mauvaise prédiction de branchement a lieu, les registres virtuels contiennent des données potentiellement invalides, contrairement aux registres architecturaux. En cas de mauvaise prédiction de branchement, la table d’alias de registres est partiellement vidée. Par vidée, on veut dire qu'on élimine les correspondances avec les registres virtuels invalidés. De plus, on doit remettre la table d’alias de registres et la table de disponibilités dans l'état antérieur au chargement de l'instruction qui a déclenché la mauvaise prédiction de branchement ou l'exception matérielle. Et cela peut se faire de différentes manières. La première solution s'applique au renommage dans le ROB. Il suffit de stocker dans le tampon de réordonnancement ce qui a été modifié dans l'unité de renommage de registres par l'instruction correspondante. Ainsi, lorsqu'une instruction sera prête à valider, et qu'une exception ou mauvaise prédiction de branchement aura eu lieu, les modifications effectuées dans l'unité de renommage de registres seront annulées les unes après les autres. Une autre solution consiste à garder un historique des changements fait dans la table d'alias. Lorsqu'une mauvais prédiction est détectée, le processeur traverse l'historique et annule les changements faits un par un. Le tout est assez lent, vu que la traversée se fait un renommage à la fois. Une optimisation utilise une copie valide de la table d’alias de registres dans une mémoire à part, pour la restaurer au besoin. Si jamais le processeur détecte un branchement, la table d’alias de registres est sauvegardée dans une mémoire intégrée au processeur. On peut utiliser plusieurs mémoires de sauvegarde de ce type, pour gérer une succession de branchements. ===La gestion des opérandes disponibles : "lecture avant émission" et "lecture après émission"=== Passons maintenant à un détail d'implémentation de l'exécution dans le désordre. La '''lecture après émission''' utilise des fenêtres d'instruction proprement dit, alors que la '''lecture avant émission''' utilise des stations de réservation. Rappelons que la différence est que les premières émettent les instruction puis lisent les registres, alors que la seconde lit les opérandes depuis les registres juste après le renommage de registre, pour les stocker dans la fenêtre d'instruction. La différence est donc que les étages sont répartis différemment. Avec la "lecture avant émission", l'étage de lecture des opérandes est placé entre l'étage de renommage de registres et l'étage d'émission. Avec la "lecture après émission", l'étage de lecture des registres est situé après l'étage d'émission/''select/wakeup''. Les deux méthodes de lecture avant ou après émission montrent une grande différence pour ce qui est de déterminer la disponibilité des opérandes. Pour la lecture après émission, le renommage de registre n'est pas impliqué. Mais avec la lecture avant émission, les opérandes sont lues dans les registres entre le renommage des registres et l'entrée dans la station de réservation. Ce qui implique qu'on sait quelles sont les opérandes disponibles lors du renommage de registres. Pour cela, l'unité de renommage de registre contient, pour chaque registre virtuel, un bit ''ready'' qui indique que le registre renommé contient une opérande est disponible. S'il est à 0, l'instruction est placée dans la fenêtre d'instruction, et attend que ses opérandes soient disponibles. S'il est à 1, les opérandes sont soit lues dans le registre et placées dans la fenêtre d'instruction, soit lues au moment d'exécuter l'instruction. L'ensemble de ces bits est en plus des bits de disponibilité de la station de réservation, dans l'implémentation la plus simple. Les deux sont alors mis à jour en même temps. Une autre solution regroupe les bits de disponibilité dans une structure matérielle appelée la '''table de disponibilité''', qui est consultée à la fois par les stations de réservation et l'unité de renommage de registres. L'avantage de cette solution est que les signaux de réveil sont envoyés à une seule structure matérielle, pas plus. De plus, il n'y a pas de duplication des bits de disponibilité. Non pas que quelques bits fassent une grosse différence, mais tout est bon à prendre. Notons qu'avec un banc de registres physique, le bit de disponibilité se déduit des quatre états que peut prendre un registre physique. L'unité de renommage de registre doit sait déjà quelles sont les opérandes indisponibles, celles qui ont déjà été calculées, et celles enregistrées dans un registre architectural. Notons qu'un registre virtuel, une fois attribué, peut être en état réservé ou écrit. Seul l'état écrit correspond à une opérande disponible, l'unité de renommage doit donc pouvoir faire la différence. Pour savoir si un registre virtuel contient une donnée valide, la table d’alias de registres lui associe un bit de validité qui est mis à jour lors de l'écriture du résultat dans le registre virtuel correspondant. Elle doit aussi mémoriser quels registres architecturaux ont une donnée valide. Rappelons que le processeur sait qu'une opérande est disponible parce qu'un signal de réveil est généré dans le pipeline, pour dire que telle opérande est disponible. Le signal transmet l'opérande avec le numéro de registre (virtuel ici). Le numéro de registre transmis est alors comparé avec tous les numéros de registre des opérandes, dans la fenêtre d'instruction et/ou les stations de réservation. Il est possible qu'une instruction soit dans l'étape de renommage alors que le signal de ''réveil'' est envoyé. Si rien n'est fait, l'instruction sera insérée dans la fenêtre d'instruction avec une opérande indisponible, alors qu'elle l'est. Elle restera alors indéfiniment dans la fenêtre d'instruction, vu que le signal de ''réveil'' n'est généré qu'une seule fois. Pour éviter cela, le signal de ''réveil'' doit aussi être envoyé aux instructions dans l'unité de renommage de registre. Une solution pour éviter cela est d'enregistrer directement les noms de registres renommés dans la fenêtre d'instruction, dès la sortie de l'unité de renommage. Les noms de registres sautent un étage de pipeline, donc. La comparaison avec les signaux de réveil se fait alors dans la fenêtre d'instruction, pas besoin de rajouter un comparateur dans l'étage de lecture des registres. Une autre solution est d'utiliser une table de disponibilité unique. Les signaux de réveil ne sont alors pas perdus. ==Les optimisations liées au renommage de registres== Le renommage de registre permet d'implémenter certaines optimisations annexes, qu'on ne croirait pas reliées au renommage de registre au premier abord. Par exemple, saviez-vous que le renommage de registre permet d'exécuter certaines instructions directement dans l'unité de renommage ? De nombreuses optimisations éliminent certaines instructions, en les remplaçant par des renommages de registres. Il existe plusieurs optimisations de ce genre, qui portent les noms barbabres d'élimination des MOV, de calcul trivial, et de remplacement d'idiomes. ===L'élimination des MOV=== La première optimisation de ce genre élimine les instructions de copie d'un registre dans un autre (les MOV) ou d’échange entre deux registres (XCHG). Ce qui explique pourquoi l'optimisation en question porte le nom d''''élimination des MOV''' (''MOV elimination''), sous-entendu, des MOV inter-registres. L'élimination des MOV est une technique implémenté sur les processeurs à banc de registre physique. En clair, il faut qu'il y ait un seul banc de registre. C'est important, car c'est la seule méthode qui fait que les registres virtuels n'ont pas à être copiés dans les registres architecturaux, ce qui facilite grandement l'implémentation d’une technique visant à éliminer des copies de registres. L'idée est qu'après une copie, le contenu des deux registres source et destination est identique tant qu'aucun des deux registres ne subit d'écriture. On peut alors utiliser un seul registre physique pour la valeur mémorisée, toute lecture des deux registres lisant celui-ci. Lors d'une écriture dans un de ces deux registres, le renommage attribuera un nouveau registre physique pour la nouvelle valeur écrite. : Formellement, il s'agit d'une optimisation de type '''''copy-on-write'''''. ===Le remplacement d'idiomes=== Le même genre d'optimisation peut être effectué avec des instructions inutiles, dont le résultat vaut zéro. En effet, il existe de nombreuses méthodes pour mettre un registre à zéro. Faire un XOR entre le registre et lui-même, le soustraire à lui-même ou le multiplier par zéro sont quelques exemples. Et de tels idiomes sont utilisés par les compilateurs, au lieu d'un simple MOV 0 -> registre, qui copie un 0 dans le registre. La raison est que les instructions MOV sont assez longues, vu qu'elles doivent intégrer une constante immédiate. Le renommage de registres peut être utilisé pour éliminer ces opérations et les remplacer par une mise à zéro du registre. L'optimisation en question porte le nom de '''remplacement d'idiomes'''. Elle est implémentée depuis les processeurs Pentium de microarchitecture P6, et leurs équivalents AMD. Un autre avantage de cette optimisation est que cela casse de fausses chaines de dépendances. Par exemple, une opération "reg XOR reg" faire croire au processeur qu'il y a une dépendance de donnée entre cette instruction, et les instructions précédente qui écrivent dans le registre. Le XOR est donc exécuté après ces instructions faussement dépendantes, alors que la mise à zéro du registre pourrait se faire en parallèle en utilisant un registre virtuel séparé. Les techniques précédentes permettent d'éliminer ces fausses dépendances. ===Les optimisations de calculs triviaux=== Il existe aussi des opérations dont le résultat est une des opérandes. On pourrait citer comme exemples les décalages par 0, les additions et soustractions avec 0, la multiplication par 1, et bien d'autres. En utilisant intelligemment le renommage de registres, ces calculs ne sont pas effectués. Les techniques qui permettent cela sont des techniques dites de '''calcul trivial''' (trivial computation). La détection des calculs simplifiables demande de comparer les opérandes avec 0 ou 1, via un paquets de comparateurs regroupés dans une unité de détection des calculs triviaux. Leur simplification se fait dans la table de renommage de registres, le registre de destination de l'instruction simplifiée étant renommé pour pointer sur l'opérande/résultat. Si le résultat est nul, on considère que la correspondance est invalide et on met le registre physique à une valeur invalide, similaire au pointeur NULL du C. Si un calcul lit un registre physique invalide, celui-ci est automatiquement simplifié par l'unité de renommage, l'autre opérande étant alors attribué automatiquement comme registre de destination du résultat dans la table de renommage. Mais cette technique a tendance à modifier la latence des instructions, qui est réduite après simplification. Cela pose problème au niveau des circuits d'émission et du planificateur, qui n'aiment pas les latences variables, comme on l'a vu il y a quelques chapitres. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les dépendances de données et l'exécution dans le désordre | prevText=Les dépendances de données et l'exécution dans le désordre | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> a3i1oagt5ua8e1ens62ysml4075oesz Fonctionnement d'un ordinateur/Les processeurs superscalaires 0 65956 745980 745874 2025-07-05T14:24:16Z Mewtow 31375 /* Un étude des microarchitectures superscalaires x86 d'Intel */ Déplacement autre chapitre 745980 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. ==L'implémentation d'un processeur superscalaire== Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose. ===Les circuits hors-ALU sont soit dupliqués, soit adaptés=== Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas. Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation. Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps. ===La duplication des unités de calcul et les contraintes d’appariement=== Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite. Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor. Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. ==L'étape de chargement superscalaire== Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6. Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. : Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block''). ===Le circuit de fusion de blocs=== Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non. Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles. [[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''. [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. ===Le cache de traces=== Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait 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. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. 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. 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. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] [[File:BasicBlocks.png|vignette|Blocs de base.]] Pour comprendre ce qu'est une trace, 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. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même. Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction. Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace. ==Le séquenceur d'un processeur superscalaire== Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées. ===Les décodeurs d'instructions superscalaires=== Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. ===L'unité de renommage superscalaire=== Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===L'unité d'émission superscalaire=== Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives. Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge. Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture. Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle. Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture. ===Les conséquences sur le banc de registre=== Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter''). ==Les unités de calcul des processeurs superscalaires== Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception. La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents. ===La double émission entière-flottante=== En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes. En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée. ===L'émission multiple des micro-opérations flottantes=== La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]] ===L'émission multiple des micro-opérations entières=== Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible. : Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]] Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. : Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]] ===L'émission multiple des accès mémoire=== Après avoir vu l'émission multiple pour les opérations flottantes et etnières, il est temps de voir l''''émission multiple des accès mémoire'''. ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée au minimum d'une unité de calcul d'adresse, des ports de lecture/écriture du cache de données. Le tout peut éventuellement être complété par des structures qui remettent en ordre les accès mémoire, comme une ''Load-store queue''. Émettre plusieurs micro-opérations mémoire demande d'avoir plusieurs unités de calcul, une par micro-opération mémoire émise par cycle. Si le processeur peut émettre trois micro-opérations mémoire à la fois, il y aura besoin de trois unités de calcul d'adresse. Chaque unité de calcul d'adresse est directement reliée à un port d'émission mémoire. Les ports de lecture/écriture du cache sont aussi dupliqués, afin de gérer plusieurs accès mémoire simultanés au cache de données. Par contre, la structure qui remet les accès mémoire en ordre n'est pas dupliquée. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas. Par contre, elle est rendue multi-ports afin de gérer plusieurs micro-opérations mémoire simultanés. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. C'est la même chose avec les processeurs avec juste une ''Store Queue'' et une ''Load Queue''. Prenons un processeur qui peut émettre une lecture et une écriture en même temps. Dans ce cas, la lecture va dans la ''Load Queue'', l'écriture dans la ''Store Queue''. Il y a bien des modifications à faire sur les deux files, afin de gérer deux accès simultanés, mais les structures ne sont pas dupliqués. Si on veut gérer plusieurs lectures ou plusieurs écritures, il suffit d'ajouter des ports à la ''Load Queue' ou à la ''Store Queue''. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente. D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures. ===L'interaction avec les fenêtres d'instruction=== Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié. [[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]] Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===Résumé=== Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports. ==Le contournement sur les processeurs superscalaires== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] ===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs=== Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple. Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Les bancs de registre sont aussi adaptés pour le contournement=== L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes. Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. ==Les optimisations de la pile d'appel : le ''stack engine''== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire au niveau du cache | prevText=Le parallélisme mémoire au niveau du cache | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> bdrh6ace9yg8qgym0zdqc291otju7s6 745982 745980 2025-07-05T14:24:39Z Mewtow 31375 /* Un étude des microarchitectures superscalaires x86 d'AMD */ Déplacement autre chapitre 745982 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. ==L'implémentation d'un processeur superscalaire== Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose. ===Les circuits hors-ALU sont soit dupliqués, soit adaptés=== Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas. Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation. Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps. ===La duplication des unités de calcul et les contraintes d’appariement=== Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite. Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor. Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. ==L'étape de chargement superscalaire== Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6. Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. : Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block''). ===Le circuit de fusion de blocs=== Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non. Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles. [[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''. [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. ===Le cache de traces=== Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait 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. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. 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. 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. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] [[File:BasicBlocks.png|vignette|Blocs de base.]] Pour comprendre ce qu'est une trace, 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. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même. Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction. Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace. ==Le séquenceur d'un processeur superscalaire== Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées. ===Les décodeurs d'instructions superscalaires=== Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. ===L'unité de renommage superscalaire=== Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===L'unité d'émission superscalaire=== Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives. Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge. Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture. Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle. Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture. ===Les conséquences sur le banc de registre=== Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter''). ==Les unités de calcul des processeurs superscalaires== Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception. La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents. ===La double émission entière-flottante=== En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes. En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée. ===L'émission multiple des micro-opérations flottantes=== La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]] ===L'émission multiple des micro-opérations entières=== Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible. : Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]] Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. : Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]] ===L'émission multiple des accès mémoire=== Après avoir vu l'émission multiple pour les opérations flottantes et etnières, il est temps de voir l''''émission multiple des accès mémoire'''. ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée au minimum d'une unité de calcul d'adresse, des ports de lecture/écriture du cache de données. Le tout peut éventuellement être complété par des structures qui remettent en ordre les accès mémoire, comme une ''Load-store queue''. Émettre plusieurs micro-opérations mémoire demande d'avoir plusieurs unités de calcul, une par micro-opération mémoire émise par cycle. Si le processeur peut émettre trois micro-opérations mémoire à la fois, il y aura besoin de trois unités de calcul d'adresse. Chaque unité de calcul d'adresse est directement reliée à un port d'émission mémoire. Les ports de lecture/écriture du cache sont aussi dupliqués, afin de gérer plusieurs accès mémoire simultanés au cache de données. Par contre, la structure qui remet les accès mémoire en ordre n'est pas dupliquée. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas. Par contre, elle est rendue multi-ports afin de gérer plusieurs micro-opérations mémoire simultanés. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. C'est la même chose avec les processeurs avec juste une ''Store Queue'' et une ''Load Queue''. Prenons un processeur qui peut émettre une lecture et une écriture en même temps. Dans ce cas, la lecture va dans la ''Load Queue'', l'écriture dans la ''Store Queue''. Il y a bien des modifications à faire sur les deux files, afin de gérer deux accès simultanés, mais les structures ne sont pas dupliqués. Si on veut gérer plusieurs lectures ou plusieurs écritures, il suffit d'ajouter des ports à la ''Load Queue' ou à la ''Store Queue''. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente. D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures. ===L'interaction avec les fenêtres d'instruction=== Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié. [[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]] Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===Résumé=== Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports. ==Le contournement sur les processeurs superscalaires== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] ===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs=== Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple. Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Les bancs de registre sont aussi adaptés pour le contournement=== L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes. Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. ==Les optimisations de la pile d'appel : le ''stack engine''== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire au niveau du cache | prevText=Le parallélisme mémoire au niveau du cache | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> mgnzntdklhrx1xu85yo0qxt2tfjtt8y 746000 745982 2025-07-05T14:58:52Z Mewtow 31375 /* Les points de synchronisation du delta */ 746000 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. ==L'implémentation d'un processeur superscalaire== Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose. ===Les circuits hors-ALU sont soit dupliqués, soit adaptés=== Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas. Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation. Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps. ===La duplication des unités de calcul et les contraintes d’appariement=== Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite. Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor. Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. ==L'étape de chargement superscalaire== Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6. Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. : Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block''). ===Le circuit de fusion de blocs=== Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non. Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles. [[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''. [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. ===Le cache de traces=== Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait 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. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. 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. 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. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] [[File:BasicBlocks.png|vignette|Blocs de base.]] Pour comprendre ce qu'est une trace, 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. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même. Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction. Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace. ==Le séquenceur d'un processeur superscalaire== Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées. ===Les décodeurs d'instructions superscalaires=== Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. ===L'unité de renommage superscalaire=== Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===L'unité d'émission superscalaire=== Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives. Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge. Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture. Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle. Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture. ===Les conséquences sur le banc de registre=== Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter''). ==Les unités de calcul des processeurs superscalaires== Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception. La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents. ===La double émission entière-flottante=== En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes. En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée. ===L'émission multiple des micro-opérations flottantes=== La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]] ===L'émission multiple des micro-opérations entières=== Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible. : Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]] Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. : Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]] ===L'émission multiple des accès mémoire=== Après avoir vu l'émission multiple pour les opérations flottantes et etnières, il est temps de voir l''''émission multiple des accès mémoire'''. ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée au minimum d'une unité de calcul d'adresse, des ports de lecture/écriture du cache de données. Le tout peut éventuellement être complété par des structures qui remettent en ordre les accès mémoire, comme une ''Load-store queue''. Émettre plusieurs micro-opérations mémoire demande d'avoir plusieurs unités de calcul, une par micro-opération mémoire émise par cycle. Si le processeur peut émettre trois micro-opérations mémoire à la fois, il y aura besoin de trois unités de calcul d'adresse. Chaque unité de calcul d'adresse est directement reliée à un port d'émission mémoire. Les ports de lecture/écriture du cache sont aussi dupliqués, afin de gérer plusieurs accès mémoire simultanés au cache de données. Par contre, la structure qui remet les accès mémoire en ordre n'est pas dupliquée. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas. Par contre, elle est rendue multi-ports afin de gérer plusieurs micro-opérations mémoire simultanés. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. C'est la même chose avec les processeurs avec juste une ''Store Queue'' et une ''Load Queue''. Prenons un processeur qui peut émettre une lecture et une écriture en même temps. Dans ce cas, la lecture va dans la ''Load Queue'', l'écriture dans la ''Store Queue''. Il y a bien des modifications à faire sur les deux files, afin de gérer deux accès simultanés, mais les structures ne sont pas dupliqués. Si on veut gérer plusieurs lectures ou plusieurs écritures, il suffit d'ajouter des ports à la ''Load Queue' ou à la ''Store Queue''. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente. D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures. ===L'interaction avec les fenêtres d'instruction=== Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié. [[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]] Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===Résumé=== Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports. ==Le contournement sur les processeurs superscalaires== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] ===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs=== Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple. Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Les bancs de registre sont aussi adaptés pour le contournement=== L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes. Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. ==Les optimisations de la pile d'appel : le ''stack engine''== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire au niveau du cache | prevText=Le parallélisme mémoire au niveau du cache | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> g2ekphw29k9vfj06u7eu3w9979scx7b 746026 746000 2025-07-05T16:21:03Z Mewtow 31375 /* La double émission entière-flottante */ 746026 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. ==L'implémentation d'un processeur superscalaire== Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose. ===Les circuits hors-ALU sont soit dupliqués, soit adaptés=== Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas. Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation. Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps. ===La duplication des unités de calcul et les contraintes d’appariement=== Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite. Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor. Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. ==L'étape de chargement superscalaire== Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6. Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris. : Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block''). ===Le circuit de fusion de blocs=== Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non. Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles. [[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''. [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle. ===Le cache de traces=== Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait 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. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. 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. 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. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] [[File:BasicBlocks.png|vignette|Blocs de base.]] Pour comprendre ce qu'est une trace, 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. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même. Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction. Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace. ==Le séquenceur d'un processeur superscalaire== Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées. ===Les décodeurs d'instructions superscalaires=== Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. ===L'unité de renommage superscalaire=== Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===L'unité d'émission superscalaire=== Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives. Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge. Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture. Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle. Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture. ===Les conséquences sur le banc de registre=== Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter''). ==Les unités de calcul des processeurs superscalaires== Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception. La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents. ===La double émission entière-flottante=== En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée. ===L'émission multiple des micro-opérations flottantes=== La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]] ===L'émission multiple des micro-opérations entières=== Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible. : Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]] Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. : Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]] ===L'émission multiple des accès mémoire=== Après avoir vu l'émission multiple pour les opérations flottantes et etnières, il est temps de voir l''''émission multiple des accès mémoire'''. ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée au minimum d'une unité de calcul d'adresse, des ports de lecture/écriture du cache de données. Le tout peut éventuellement être complété par des structures qui remettent en ordre les accès mémoire, comme une ''Load-store queue''. Émettre plusieurs micro-opérations mémoire demande d'avoir plusieurs unités de calcul, une par micro-opération mémoire émise par cycle. Si le processeur peut émettre trois micro-opérations mémoire à la fois, il y aura besoin de trois unités de calcul d'adresse. Chaque unité de calcul d'adresse est directement reliée à un port d'émission mémoire. Les ports de lecture/écriture du cache sont aussi dupliqués, afin de gérer plusieurs accès mémoire simultanés au cache de données. Par contre, la structure qui remet les accès mémoire en ordre n'est pas dupliquée. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas. Par contre, elle est rendue multi-ports afin de gérer plusieurs micro-opérations mémoire simultanés. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. C'est la même chose avec les processeurs avec juste une ''Store Queue'' et une ''Load Queue''. Prenons un processeur qui peut émettre une lecture et une écriture en même temps. Dans ce cas, la lecture va dans la ''Load Queue'', l'écriture dans la ''Store Queue''. Il y a bien des modifications à faire sur les deux files, afin de gérer deux accès simultanés, mais les structures ne sont pas dupliqués. Si on veut gérer plusieurs lectures ou plusieurs écritures, il suffit d'ajouter des ports à la ''Load Queue' ou à la ''Store Queue''. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Les processeurs AMD K6 sont similaires, avec un port d'émission pour les lectures et un autre pour les écritures. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une ''Store Queue'', une version simplifiée de la LSQ dédiée aux écritures. Le processeur exécutait les lectures dès qu'elles avaient leurs opérandes de disponibles, seules les écritures étaient mises en attente. D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures. ===L'interaction avec les fenêtres d'instruction=== Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié. [[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]] Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===Résumé=== Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports. ==Le contournement sur les processeurs superscalaires== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] ===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs=== Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple. Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Les bancs de registre sont aussi adaptés pour le contournement=== L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes. Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. ==Les optimisations de la pile d'appel : le ''stack engine''== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire au niveau du cache | prevText=Le parallélisme mémoire au niveau du cache | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> c8a5oxyuuvp3t6va1dbi85y5smpl6ge Fonctionnement d'un ordinateur/Les mémoires cache 0 65957 745975 745796 2025-07-05T12:59:05Z Mewtow 31375 /* L'impact du cache d'instruction sur les performances */ 745975 wikitext text/x-wiki Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ==La hiérarchie mémoire des caches== [[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]] On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ==Un exemple de cache : le cache d'instruction== La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===Le prédécodage d'instructions=== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Le cache d'instruction est souvent en lecture seule=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les technologies RAID | prevText=Les technologies RAID | next=Le préchargement | nextText=Le préchargement }} </noinclude> ckczcush887fhbj9ead15duevfwd3sp 745976 745975 2025-07-05T13:00:15Z Mewtow 31375 /* Un exemple de cache : le cache d'instruction */ 745976 wikitext text/x-wiki Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ==La hiérarchie mémoire des caches== [[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]] On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ==Un exemple de cache : le cache d'instruction== La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===Le prédécodage d'instructions=== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les technologies RAID | prevText=Les technologies RAID | next=Le préchargement | nextText=Le préchargement }} </noinclude> h5kn1ouhehcwrhiyjipyrgoeizpcrpk 745977 745976 2025-07-05T13:44:45Z Mewtow 31375 /* Un exemple de cache : le cache d'instruction */ 745977 wikitext text/x-wiki Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ==La hiérarchie mémoire des caches== [[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]] On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ==Un exemple de cache : le cache d'instruction== La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. ===Le prédécodage d'instructions=== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les technologies RAID | prevText=Les technologies RAID | next=Le préchargement | nextText=Le préchargement }} </noinclude> 3ehb97e4m69zld8grc91j6o5aqwyyd6 Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire 0 68397 745986 744274 2025-07-05T14:26:01Z Mewtow 31375 /* L'exemple avec le x86 */ 745986 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... 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''. D'autres 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, ce qui garantit que les écritures et lectures s'exécutent l'une après l'autre. 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 va lire une donnée en mémoire, va comparer celle-ci à l'opérande de notre instruction (une donnée fournie par l'instruction), et va écrire un résultat en mémoire si ces deux valeurs sont différentes. Ce fameux résultat est fourni par l'instruction, ou est stocké dans un registre du processeur. |- ! Fetch And Add | Cette instruction charge la valeur de notre compteur depuis la mémoire, l'incrémente, et écrit sa valeur en une seule fois. Elle permet de réaliser ce qu'on appelle des sémaphores. Elle permet aussi d'implémenter des compteurs concurrents. |- ! XCHG | Cette instruction peut échanger le contenu d'un registre et d'un morceau de mémoire de façon atomique. Elle est notoirement utilisée sur les processeurs x86 de nos PC, qui implémentent cette instruction. |} 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 en configurant un bit du bus mémoire, ou par d'autres mécanismes de synchronisation entre processeurs. Le cout de ce blocage de la mémoire est assez lourd, ce qui rend les instructions atomiques assez lentes. Mais on peut optimiser le cas où la donnée est dans le cache, en état ''Modified'' ou ''Exclusive''. Dans ce cas, pas besoin de bloquer la mémoire. Le processeur a juste à écrire dans la mémoire cache, et les mécanismes de cohérence des caches se contenteront de mettre à jour la donnée de façon atomique automatiquement. Le coût des instructions atomiques est alors fortement amorti. 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. A 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=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 7ah0rytkiuwmoiv7lo1abw96k7g9l1o 745999 745986 2025-07-05T14:58:10Z Mewtow 31375 /* L'exemple avec le x86 */ 745999 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... 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''. D'autres 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, ce qui garantit que les écritures et lectures s'exécutent l'une après l'autre. 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 va lire une donnée en mémoire, va comparer celle-ci à l'opérande de notre instruction (une donnée fournie par l'instruction), et va écrire un résultat en mémoire si ces deux valeurs sont différentes. Ce fameux résultat est fourni par l'instruction, ou est stocké dans un registre du processeur. |- ! Fetch And Add | Cette instruction charge la valeur de notre compteur depuis la mémoire, l'incrémente, et écrit sa valeur en une seule fois. Elle permet de réaliser ce qu'on appelle des sémaphores. Elle permet aussi d'implémenter des compteurs concurrents. |- ! XCHG | Cette instruction peut échanger le contenu d'un registre et d'un morceau de mémoire de façon atomique. Elle est notoirement utilisée sur les processeurs x86 de nos PC, qui implémentent cette instruction. |} 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 en configurant un bit du bus mémoire, ou par d'autres mécanismes de synchronisation entre processeurs. Le cout de ce blocage de la mémoire est assez lourd, ce qui rend les instructions atomiques assez lentes. Mais on peut optimiser le cas où la donnée est dans le cache, en état ''Modified'' ou ''Exclusive''. Dans ce cas, pas besoin de bloquer la mémoire. Le processeur a juste à écrire dans la mémoire cache, et les mécanismes de cohérence des caches se contenteront de mettre à jour la donnée de façon atomique automatiquement. Le coût des instructions atomiques est alors fortement amorti. 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. A 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=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> l03umm83jvsm2w65naqgmofkxcqa9oo Fonctionnement d'un ordinateur/Sommaire 0 69596 745978 744631 2025-07-05T14:22:03Z Mewtow 31375 /* Annexes */ 745978 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]] ===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 circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]] * [[Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence|Les timers et diviseurs de fréquence]] ===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 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 de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[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 flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] ===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]] ===Les jeux d'instruction spécialisés ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] * [[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 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]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Les technologies RAID|Les technologies RAID]] ==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]] * [[Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques|Les pipelines de longueur fixe et dynamiques]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/Les exceptions précises et branchements|Les exceptions précises et branchements]] * [[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 le désordre=== * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Les dépendances de données et l'exécution dans le désordre|Les dépendances de données et l'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Annexe : Le scoreboarding et l'algorithme de Tomasulo]] ===Les accès mémoire avec un pipeline=== * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre|Les unités mémoires à exécution dans l'ordre]] * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre|Les unités mémoires à exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache|Le parallélisme mémoire au niveau du cache]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== * [[Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86|Exemples de microarchitectures CPU : le cas du x86]] * [[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 architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] * [[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}} d7hftns5edggr8pn38widwb1i6ielrg 745996 745978 2025-07-05T14:57:22Z Mewtow 31375 /* Annexes */ 745996 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]] ===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 circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]] * [[Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence|Les timers et diviseurs de fréquence]] ===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 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 de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[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 flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] ===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]] ===Les jeux d'instruction spécialisés ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] * [[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 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]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Les technologies RAID|Les technologies RAID]] ==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]] * [[Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques|Les pipelines de longueur fixe et dynamiques]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/Les exceptions précises et branchements|Les exceptions précises et branchements]] * [[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 le désordre=== * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Les dépendances de données et l'exécution dans le désordre|Les dépendances de données et l'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Annexe : Le scoreboarding et l'algorithme de Tomasulo]] ===Les accès mémoire avec un pipeline=== * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre|Les unités mémoires à exécution dans l'ordre]] * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre|Les unités mémoires à exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache|Le parallélisme mémoire au niveau du cache]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== * [[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 architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] * [[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}} nv8ajpb8jertfmyfj7vz7ldy6q4ebd1 745997 745996 2025-07-05T14:57:41Z Mewtow 31375 /* L'émission multiple */ 745997 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]] ===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 circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]] * [[Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence|Les timers et diviseurs de fréquence]] ===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 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 de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[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 flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] ===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]] ===Les jeux d'instruction spécialisés ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] * [[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 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]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Les technologies RAID|Les technologies RAID]] ==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]] * [[Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques|Les pipelines de longueur fixe et dynamiques]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/Les exceptions précises et branchements|Les exceptions précises et branchements]] * [[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 le désordre=== * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Les dépendances de données et l'exécution dans le désordre|Les dépendances de données et l'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Annexe : Le scoreboarding et l'algorithme de Tomasulo]] ===Les accès mémoire avec un pipeline=== * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre|Les unités mémoires à exécution dans l'ordre]] * [[Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre|Les unités mémoires à exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache|Le parallélisme mémoire au niveau du cache]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86|Exemples de CPU superscalaires: le cas du x86]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== * [[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 architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] * [[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}} a91gs1ral21kaumgeggy7wyrsovupf6 Néerlandais/Pour adultes/Progressons pas à pas/Leçon 8 : L'automne 0 70296 746075 740279 2025-07-06T04:14:00Z 2A02:8440:714B:E27A:2FB3:E316:8917:DDFB /* L'automne */Correction de "quelque temps" 746075 wikitext text/x-wiki {{Néerlandais-Page-Suivante|Prec=Leçon 7 : Les hobbys|Mid=Pour adultes/Progressons pas à pas|Suiv=Leçon 9 : Modalité |TDM=[[Néerlandais/Pour adultes/Progressons pas à pas|Progressons pas à pas]]}} [[File:Red Squirrel - geograph.org.uk - 185008.jpg|thumb|{{knop|eekhoorn}} De eekhoorn]] [[File:Pica pica -Daventry Country Park, Daventry, Northamptonshire, England-8.jpg|thumb|{{knop|ekster}} De ekster|left]] [[File:Dr_Priyantha_Udagedara.jpg|thumb|{{knop|bloempjes}} De bloempjes|left]] [[File:Hazelnuts.jpg|thumb|{{knop|nootjes}} De nootjes]] == L'automne == {| |Het was herfst, met al zijn geuren en dode bladeren. |C’était l’automne, avec son ballet d’odeurs et de feuilles mortes. |- |In het bos klom een eekhoorn met moeite langs een boomstam omhoog. |Dans la forêt, un écureuil remontait péniblement le long d’un tronc d’arbre. |- |Beladen met nootjes haastte hij zich om zijn wintervoorraad op te bouwen. |Chargé de noisettes, il se pressait de faire ses réserves pour l’hiver. |- |Op een nabije tak landde er een ekster. Bevreesd dat deze zijn hapje zou stelen vervolgde de eekhoorn zijn weg, de vogel met wantrouwen in de gaten houdend. |Sur une branche près de lui, une pie se posa. De peur qu’elle ne lui volât sa pitance, l’écureuil poursuivit son chemin en l’observant avec méfiance. |- |De ekster opende zijn snavel om te zingen, maar hij kon geen geluid voortbrengen. |La pie ouvrit le bec pour chanter mais elle ne put émettre aucun son. |- |In plaats daarvan spoot er een straal kleine bloempjes tevoorschijn. |À la place, jaillit un jet de petites fleurs. |- |De eekhoorn was erg verbaasd en de ekster vierkant met stomheid geslagen. |L’écureuil fut fort étonné et la pie carrément stupéfaite. |- |In een poging tot protest lukte het hem slechts een wolk dode blaadjes voor te brengen. |Cherchant à protester, elle ne parvint à produire qu’un nuage de feuilles mortes. |- |De eekhoorn barstte in lachen uit, wat de ekster niet erg kon waarderen. |L’écureuil éclata de rire, ce qui n’amusa guère la pie. |- |In paniek probeerde hij nog iets uit te schreeuwen maar handenvol kleurrijke bloemen ontsproten uit zijn bek. |Paniquée, elle voulut crier quelque chose, mais des brassées de fleurs multicolores s’échappèrent de son gosier. |- |Daarop liet hij ontmoedigd zijn hoofd zakken. |Alors elle baissa la tête, découragée. |- |De eekhoorn liet zijn nootjes los en ging naar de ekster toe, met spijt dat hij hem gekwetst had. |L’écureuil lâcha ses noisettes et s’approcha de la pie, désolé qu’il fût de lui avoir causé de la peine. |- |Een andere ekster, die het schouwspel gezien had, lachte zijn ongelukkige soortgenoot uit en vloog honend weg. |Une autre pie, qui avait vu la scène, se moqua de la malheureuse puis s’envola en ricanant. |- |Andere eekhoorns verzamelden zich echter om de arme ekster heen om hem te troosten. |D’autres écureuils s’approchèrent de la pauvre pie pour la réconforter. |- |Deze bleef nog enige tijd bij de eekhoorns, zijn nieuwe vrienden, nu dat zijn mede-eksters besloten hadden hem de rug toe te keren. |Elle resta quelque temps auprès des écureuils, ses nouveaux amis, les autres pies ayant décidé de la rejeter.<ref>{{Lien web|url=http://www.historiettes.fr/25/|titre={{Auteur|Stéphane Joret}}}}</ref> |} == Vocabulaire == {|class="wikitable" !colspan="4"|Avec "de" |- |{{knop|herfst}} de herfst -- l'automne||{{knop|geur}} de geur -- l'odeur||{{knop|moeite}} de moeite -- l'effort ||{{knop|noot}} de noot -- la noix |- |{{knop|boomstam}} de boomstam - tronc d'arbre||{{knop|winter}}de winter - l'hiver||{{knop|voorraad}} de voorraad -- réserve||{{knop|tak}} de tak -- branche |- |{{knop|weg}} de weg -- chemin|| {{knop|vogel}} de vogel-- oiseau ||{{knop|snavel}} de snavel - bec (oiseau)||{{knop|straal}} de straal -- jet, ray |- |{{knop|stomheid}} de stomheid -- mutité||{{knop|wolk}} de wolk -- nuage||{{knop|bek}} de bek - bec, gueule ||{{knop|spijt}} de spijt - regret |- !colspan="4"|Avec "het" |- |{{knop|bos}} het bos - forêt ||{{knop|blad}} het blad -- la feuille|| {{knop|wantrouwen}} het wantrouwen -- méfiance ||{{knop|gat}} het gat -- trou, (ici: l’œil) |- ||{{knop|geluid}} het geluid -- son||{{knop|protest}} het protest||{{knop|hoofd}} het hoofd -- tête |} == La leçon de grammaire : le passé (imparfait) ou prétérit == Le passé (''onvoltooid verleden tijd o.v.t.'') est encore plus simple que le présent (''onvoltooid tegenwoordige tijd o.t.t.''), car il n'y a que deux formes : un singulier et un pluriel. Contrairement au français il n'y a pas de différence entre l'imparfait et le passé simple. Par exemple le verbe ''landen'' (atterir) : {|class="wikitable" |- !Pronom !! Présent !! Passé |- |Ik || land || rowspan="2"|{{knop|landde}} ''landde'' |- |Jij/u|| landt |- |Wij,jullie, zij||landen||{{knop|landden}} ''landden'' |} === Le passé en -de(n) === Malheureusement pour quelqu'un qui voudrait apprendre la langue, il y a plusieurs manières de former le passé. Il y a une douzaine de groupes. Les verbes ''landen'', ''vervolgen'', ''openen'', ''verzamelen'' et ''proberen'' dans notre histoire d'automne en représentent la groupe la plus grande: les verbes ''faibles en -de''. Cette groupe forme le passé en ajoutant le suffixe ''-de(n)'' au ''radical'' du verbe. Le radical est identique à la forme du présent 1<sup>ère</sup> personne : ''ik land'', et à l'impératif: , ''land!'' Alors le passé de ''proberen'' sera probeer''de'', probeer''den''. On peut ajouter que le participe du parfait est formé en ajoutant le suffixe -d ensemble avec le préfixe ge-. Toutes les formes d'un verbe néerlandais peuvent se dériver de trois formes: {|class="wikitable" !Infinitif!!Passé!!Participe |- |landen||land'''de'''||'''ge'''lan'''d*''' |- |openen||open'''de'''||'''ge'''open'''d''' |- |proberen||probeer'''de'''||'''ge'''probeer'''d''' |- |vervolgen||vervolg'''de'''||vervolg'''d**''' |- |verzamelen||verzamel'''de'''||verzamel'''d**''' |} *Notons que l'orthographe néerlandais ne permet pas d'écrire -dd ou -tt à la fin dun mot. **Notons que le préfixe ver- supprime l'addition du préfixe ge- dans ce cas. Nou avons noté avant que le radical et la racine sont identiques sauf pour les racines finissant en v et z. Ces verbes prennent -de(n) mais le suffixe est ajouté au radical et non pas à la racine. :vre'''z'''en → Racine vree'''z'''- :vree'''s'''! → Radical vrees :ik vree'''s'''de → Passé: radical + -de :dur'''v'''en → Racine dur'''v'''- :dur'''f'''! → Radical durf :ik dur'''f'''de → Passé: radical + -de === Les autres groupes === Les autres groupes sont: :Faible en -te :Faible en -cht :Fort de classe 1 :Fort de classe 2 :Fort de classe 3 :Fort de classe 4 :Fort de classe 5 :Fort de classe 6 :Fort de classe 7 :Mixte :Irrégulier On va les étudier plus tard. Pour le moment, notons les formes du passé dans notre histoire d'automne. {|class="wikitable" !Infinitif!!Passé!!Traduction |- |{{knop|klimmen}} Klimmen|| {{knop|klom}} klom|| grimper, monter |- |{{knop|haasten}} Haasten||{{knop|haastte}} haastte|| se presser |- |{{knop|vervolgen}} Vervolgen||{{knop|vervolgde}} vervolgde||continuer |- |{{knop|openen}} Openen||{{knop|opende}} opende||ouvrir |- |{{knop|kunnen}} kunnen||{{knop|kon}} kon||pouvoir |- |{{knop|spuiten}} spuiten||{{knop|spoot}} spoot||asperger, jaillir |- |{{knop|zijn}} zijn||{{knop|was}} was||etre |- |{{knop|lukken}} lukken||{{knop|lukte}} lukte||reussir |- |{{knop|uitbarsten}} uitbarsten||{{knop|barstte uit}} barstte uit||éclater |- |{{knop|proberen}} proberen||{{knop|probeerde}} probeerde||essayer |- |{{knop|ontspruiten}} ontspruiten||{{knop|ontsproot}} ontsproot||pousser, surgir |- |{{knop|laten}} laten||{{knop|liet}} liet||laisser |- |{{knop|gaan}} gaan||{{knop|ging}} ging||aller |- |{{knop|hebben}} hebben||{{knop|had}} had||avoir |- |{{knop|lachen}} lachen||{{knop|lachte}} lachte||rire |- |{{knop|wegvliegen}} wegvliegen||{{knop|vloog weg}} vloog weg ||s'envoler |- |{{knop|verzamelen}} verzamelen||{{knop|verzamelde}} verzamelde||rassembler |- |{{knop|blijven}} blijven||{{knop|bleef}} bleef||rester |} == En profondeur == *[[Grammaire néerlandaise/le verbe/l'indicatif/le prétérit ou imparfait]] *[[Néerlandais/Guide de conversation/Les mois]] {{NL-navigation}} <references /> [[Catégorie:Progressons pas à pas en néerlandais (livre)|Leçon 8 : L'automne]] 7v88168dr3mygk5b1jei3oryh0yet7w Affaire Priore/Le Calendrier de l'Affaire Priore/2007 0 73562 746043 625881 2025-07-05T19:34:50Z Regimminius 7153 Ortho 746043 wikitext text/x-wiki {{Affaire Priore - Sommaire}} <br /> {{Affaire Priore - Calendrier}} <br /> <center><big><big>'''2007'''</big></big></center> <br /> 2007 NON-DATEE :- Article de Mme D. RIVIERE paru dans le Bulletin de la Société Historique et Archéologique du Périgord. Elle reflète les actions de son mari, le Pr. M.-R. RIVIERE (alors âgé de 16 ans) dans la résistance (1944-1945). [[Catégorie:Affaire Priore (livre)]] 1qujraltwemy5q7cqv2iaxno71342kv Fonctionnement d'un ordinateur/Version imprimable 2 0 78962 745987 744635 2025-07-05T14:27:36Z Mewtow 31375 745987 wikitext text/x-wiki __NOTOC__ {{:Fonctionnement d'un ordinateur/Introduction}} =Le codage des informations= {{:Fonctionnement d'un ordinateur/L'encodage des données}} {{:Fonctionnement d'un ordinateur/Le codage des nombres}} {{:Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur}} =Les circuits électroniques= {{:Fonctionnement d'un ordinateur/Les portes logiques}} ==Les circuits combinatoires== {{:Fonctionnement d'un ordinateur/Les circuits combinatoires}} {{:Fonctionnement d'un ordinateur/Les circuits de masquage}} {{:Fonctionnement d'un ordinateur/Les circuits de sélection}} ==Les circuits séquentiels== {{:Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit}} {{:Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones}} {{:Fonctionnement d'un ordinateur/Les registres et mémoires adressables}} {{:Fonctionnement d'un ordinateur/Les circuits compteurs et décompteurs}} {{:Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence}} ==Les circuits de calcul et de comparaison== {{:Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation}} {{:Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction}} {{:Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit}} {{:Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande}} {{:Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul flottant}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques}} {{:Fonctionnement d'un ordinateur/Les circuits de comparaison}} {{:Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique}} ==Les circuits intégrés== {{:Fonctionnement d'un ordinateur/Les transistors et portes logiques}} {{:Fonctionnement d'un ordinateur/Les circuits intégrés}} {{:Fonctionnement d'un ordinateur/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}} {{:Fonctionnement d'un ordinateur/La hiérarchie mémoire}} {{:Fonctionnement d'un ordinateur/La performance d'un ordinateur}} {{:Fonctionnement d'un ordinateur/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 bus et liaisons point à point= {{:Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS}} {{:Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)}} {{:Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus}} {{:Fonctionnement d'un ordinateur/Les liaisons point à point}} {{:Fonctionnement d'un ordinateur/Les bus électroniques}} {{:Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point}} =Les mémoires= {{:Fonctionnement d'un ordinateur/Les différents types de mémoires}} {{:Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique}} {{:Fonctionnement d'un ordinateur/Le bus mémoire}} ==La micro-architecture d'une mémoire adressable== {{:Fonctionnement d'un ordinateur/Les cellules mémoires}} {{:Fonctionnement d'un ordinateur/Le plan mémoire}} {{:Fonctionnement d'un ordinateur/Contrôleur mémoire interne}} {{:Fonctionnement d'un ordinateur/Mémoires évoluées}} ==Les mémoires primaires== {{:Fonctionnement d'un ordinateur/Les mémoires ROM}} {{:Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones}} {{:Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)}} {{:Fonctionnement d'un ordinateur/Contrôleur mémoire externe}} ==Les mémoires exotiques== {{:Fonctionnement d'un ordinateur/Les mémoires associatives}} {{:Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO}} =Le processeur= ==L'architecture externe== {{:Fonctionnement d'un ordinateur/Langage machine et assembleur}} {{:Fonctionnement d'un ordinateur/Les registres du processeur}} {{:Fonctionnement d'un ordinateur/Les modes d'adressage}} {{:Fonctionnement d'un ordinateur/L'encodage des instructions}} {{:Fonctionnement d'un ordinateur/Les jeux d'instructions}} {{:Fonctionnement d'un ordinateur/La pile d'appel et les fonctions}} {{:Fonctionnement d'un ordinateur/Les interruptions et exceptions}} {{:Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme}} ==La micro-architecture== {{:Fonctionnement d'un ordinateur/Les composants d'un processeur}} {{:Fonctionnement d'un ordinateur/Le chemin de données}} {{:Fonctionnement d'un ordinateur/L'unité de chargement et le program counter}} {{:Fonctionnement d'un ordinateur/L'unité de contrôle}} ==Les jeux d’instructions spécialisés== {{:Fonctionnement d'un ordinateur/Les architectures à accumulateur}} {{:Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins}} {{:Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire}} {{:Fonctionnement d'un ordinateur/Les processeurs de traitement du signal}} {{:Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement}} ==La mémoire virtuelle et la protection mémoire== {{:Fonctionnement d'un ordinateur/L'espace d'adressage du processeur}} {{:Fonctionnement d'un ordinateur/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}} {{:Fonctionnement d'un ordinateur/L'adressage des périphériques}} {{:Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension}} =Les mémoires de masse= {{:Fonctionnement d'un ordinateur/Les mémoires de masse : généralités}} {{:Fonctionnement d'un ordinateur/Les disques durs}} {{:Fonctionnement d'un ordinateur/Les solid-state drives}} {{:Fonctionnement d'un ordinateur/Les disques optiques}} {{:Fonctionnement d'un ordinateur/Les technologies RAID}} =La mémoire cache= {{:Fonctionnement d'un ordinateur/Les mémoires cache}} {{:Fonctionnement d'un ordinateur/Le préchargement}} {{:Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer}} =Le parallélisme d’instructions= {{:Fonctionnement d'un ordinateur/Le pipeline}} {{:Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques}} ==Les branchements et le front-end== {{:Fonctionnement d'un ordinateur/Les exceptions précises et branchements}} {{:Fonctionnement d'un ordinateur/La prédiction de branchement}} {{:Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions}} ==L’exécution dans le désordre== {{:Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions}} {{:Fonctionnement d'un ordinateur/Les dépendances de données et l'exécution dans le désordre}} {{:Fonctionnement d'un ordinateur/Le renommage de registres}} {{:Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo}} ==Les accès mémoire avec un pipeline== {{:Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre}} {{:Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre}} {{:Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache}} ==L'émission multiple== {{:Fonctionnement d'un ordinateur/Les processeurs superscalaires}} {{:Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC}} {{:Fonctionnement d'un ordinateur/Les architectures dataflow}} =Les architectures parallèles= {{:Fonctionnement d'un ordinateur/Les architectures parallèles}} {{:Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs}} {{:Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading}} {{:Fonctionnement d'un ordinateur/Les architectures à parallélisme de données}} {{:Fonctionnement d'un ordinateur/La cohérence des caches}} {{:Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire}} =Annexes= {{:Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86}} {{:Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation}} {{:Fonctionnement d'un ordinateur/Le matériel réseau}} {{:Fonctionnement d'un ordinateur/La tolérance aux pannes}} {{:Fonctionnement d'un ordinateur/Les architectures systoliques}} {{:Fonctionnement d'un ordinateur/Les réseaux de neurones matériels}} {{:Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires}} {{:Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires}} {{:Fonctionnement d'un ordinateur/Les circuits réversibles}} {{autocat}} bh8w8nbcm792shn6fzm56ik9btru5ac 746006 745987 2025-07-05T15:01:14Z Mewtow 31375 746006 wikitext text/x-wiki __NOTOC__ {{:Fonctionnement d'un ordinateur/Introduction}} =Le codage des informations= {{:Fonctionnement d'un ordinateur/L'encodage des données}} {{:Fonctionnement d'un ordinateur/Le codage des nombres}} {{:Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur}} =Les circuits électroniques= {{:Fonctionnement d'un ordinateur/Les portes logiques}} ==Les circuits combinatoires== {{:Fonctionnement d'un ordinateur/Les circuits combinatoires}} {{:Fonctionnement d'un ordinateur/Les circuits de masquage}} {{:Fonctionnement d'un ordinateur/Les circuits de sélection}} ==Les circuits séquentiels== {{:Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit}} {{:Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones}} {{:Fonctionnement d'un ordinateur/Les registres et mémoires adressables}} {{:Fonctionnement d'un ordinateur/Les circuits compteurs et décompteurs}} {{:Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence}} ==Les circuits de calcul et de comparaison== {{:Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation}} {{:Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction}} {{:Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit}} {{:Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande}} {{:Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul flottant}} {{:Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques}} {{:Fonctionnement d'un ordinateur/Les circuits de comparaison}} {{:Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique}} ==Les circuits intégrés== {{:Fonctionnement d'un ordinateur/Les transistors et portes logiques}} {{:Fonctionnement d'un ordinateur/Les circuits intégrés}} {{:Fonctionnement d'un ordinateur/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}} {{:Fonctionnement d'un ordinateur/La hiérarchie mémoire}} {{:Fonctionnement d'un ordinateur/La performance d'un ordinateur}} {{:Fonctionnement d'un ordinateur/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 bus et liaisons point à point= {{:Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS}} {{:Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)}} {{:Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus}} {{:Fonctionnement d'un ordinateur/Les liaisons point à point}} {{:Fonctionnement d'un ordinateur/Les bus électroniques}} {{:Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point}} =Les mémoires= {{:Fonctionnement d'un ordinateur/Les différents types de mémoires}} {{:Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique}} {{:Fonctionnement d'un ordinateur/Le bus mémoire}} ==La micro-architecture d'une mémoire adressable== {{:Fonctionnement d'un ordinateur/Les cellules mémoires}} {{:Fonctionnement d'un ordinateur/Le plan mémoire}} {{:Fonctionnement d'un ordinateur/Contrôleur mémoire interne}} {{:Fonctionnement d'un ordinateur/Mémoires évoluées}} ==Les mémoires primaires== {{:Fonctionnement d'un ordinateur/Les mémoires ROM}} {{:Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones}} {{:Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)}} {{:Fonctionnement d'un ordinateur/Contrôleur mémoire externe}} ==Les mémoires exotiques== {{:Fonctionnement d'un ordinateur/Les mémoires associatives}} {{:Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO}} =Le processeur= ==L'architecture externe== {{:Fonctionnement d'un ordinateur/Langage machine et assembleur}} {{:Fonctionnement d'un ordinateur/Les registres du processeur}} {{:Fonctionnement d'un ordinateur/Les modes d'adressage}} {{:Fonctionnement d'un ordinateur/L'encodage des instructions}} {{:Fonctionnement d'un ordinateur/Les jeux d'instructions}} {{:Fonctionnement d'un ordinateur/La pile d'appel et les fonctions}} {{:Fonctionnement d'un ordinateur/Les interruptions et exceptions}} {{:Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme}} ==La micro-architecture== {{:Fonctionnement d'un ordinateur/Les composants d'un processeur}} {{:Fonctionnement d'un ordinateur/Le chemin de données}} {{:Fonctionnement d'un ordinateur/L'unité de chargement et le program counter}} {{:Fonctionnement d'un ordinateur/L'unité de contrôle}} ==Les jeux d’instructions spécialisés== {{:Fonctionnement d'un ordinateur/Les architectures à accumulateur}} {{:Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins}} {{:Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire}} {{:Fonctionnement d'un ordinateur/Les processeurs de traitement du signal}} {{:Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement}} ==La mémoire virtuelle et la protection mémoire== {{:Fonctionnement d'un ordinateur/L'espace d'adressage du processeur}} {{:Fonctionnement d'un ordinateur/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}} {{:Fonctionnement d'un ordinateur/L'adressage des périphériques}} {{:Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension}} =Les mémoires de masse= {{:Fonctionnement d'un ordinateur/Les mémoires de masse : généralités}} {{:Fonctionnement d'un ordinateur/Les disques durs}} {{:Fonctionnement d'un ordinateur/Les solid-state drives}} {{:Fonctionnement d'un ordinateur/Les disques optiques}} {{:Fonctionnement d'un ordinateur/Les technologies RAID}} =La mémoire cache= {{:Fonctionnement d'un ordinateur/Les mémoires cache}} {{:Fonctionnement d'un ordinateur/Le préchargement}} {{:Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer}} =Le parallélisme d’instructions= {{:Fonctionnement d'un ordinateur/Le pipeline}} {{:Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques}} ==Les branchements et le front-end== {{:Fonctionnement d'un ordinateur/Les exceptions précises et branchements}} {{:Fonctionnement d'un ordinateur/La prédiction de branchement}} {{:Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions}} ==L’exécution dans le désordre== {{:Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions}} {{:Fonctionnement d'un ordinateur/Les dépendances de données et l'exécution dans le désordre}} {{:Fonctionnement d'un ordinateur/Le renommage de registres}} {{:Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo}} ==Les accès mémoire avec un pipeline== {{:Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre}} {{:Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre}} {{:Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache}} ==L'émission multiple== {{:Fonctionnement d'un ordinateur/Les processeurs superscalaires}} {{:Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86}} {{:Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC}} {{:Fonctionnement d'un ordinateur/Les architectures dataflow}} =Les architectures parallèles= {{:Fonctionnement d'un ordinateur/Les architectures parallèles}} {{:Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs}} {{:Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading}} {{:Fonctionnement d'un ordinateur/Les architectures à parallélisme de données}} {{:Fonctionnement d'un ordinateur/La cohérence des caches}} {{:Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire}} =Annexes= {{:Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation}} {{:Fonctionnement d'un ordinateur/Le matériel réseau}} {{:Fonctionnement d'un ordinateur/La tolérance aux pannes}} {{:Fonctionnement d'un ordinateur/Les architectures systoliques}} {{:Fonctionnement d'un ordinateur/Les réseaux de neurones matériels}} {{:Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires}} {{:Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires}} {{:Fonctionnement d'un ordinateur/Les circuits réversibles}} {{autocat}} b8utv4fu08fusocupny5ua49m27p2jh Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC 0 79441 746001 734061 2025-07-05T14:59:01Z Mewtow 31375 /* Les branchements */ 746001 wikitext text/x-wiki Dans les chapitres précédents, nous avons parlé des processeurs qui sont capables d’exécuter des instructions dans le désordre, ou plusieurs instructions en parallèle, en même temps. Mais nous nous sommes concentrés sur les processeurs avec un jeu d'instruction normal, où l’exécution des instructions en parallèle est invisible pour le programmeur ou le compilateur. Les processeurs superscalaires sont de ce type, mais ils permettent non seulement d’exécuter plusieurs instructions en même temps, mais aussi d'émettre plusieurs instructions en même temps sur des unités de calcul différentes. Le parallélisme d'instruction maximal avec ce genre d’architectures est donc un processeur pipeliné, superscalaire, à exécution dans le désordre avec renommage de registre. Mai ces architectures ont un défaut majeur, au-delà de la prédiction des branchements : elles doivent détecter les dépendances et déterminer quelles sont les instructions exécutables en parallèle. Mais il se trouve que ces informations sur les dépendances d'instructions sont connues des compilateurs modernes. Lors de la compilation, le code source est traduit en une représentation intermédiaire où les dépendances de type WAR, WAW et RAR n'existent tout simplement pas. Il faut dire que la représentation intermédiaire a souvent une infinité de registres, voire fonctionne sans registres et avec une infinité de places mémoires. De fait, les processeurs à exécution dans le désordre doivent retrouver des informations qui étaient connues du compilateur mais ont disparu du code source. De cette observation est venue l'idée de modifier le jeu d’instruction pour que ces informations sur les dépendances soient disponibles directement dans le code machine lui-même. Ainsi, le compilateur fait tout le travail de réorganisation des instructions et d’exécution en parallèle, à la place du processeur. De telles architectures s'appellent des '''architectures à parallélisme d'instruction explicite'''. Il existe plusieurs types d'architectures de ce genre, les deux principales étant les architectures VLIW et ''dataflow''. Il faut aussi mentionner les architectures découplées, fort confidentielles et peu nombreuses, que nous verrons à la fin du chapitre. Les architectures ''dataflow'' seront vues dans le chapitre suivant. L'idée derrière ces architectures est d'expliciter les dépendances entre instruction et de les encoder directement dans le code machine. Le processeur peut ainsi déterminer quelle instruction est prête à s’exécuter en fonction de la disponibilité des opérandes. L’exécution dans le désordre est donc réalisée par le compilateur. Dans ce chapitre, nous allons voir des architectures à parallélisme d'instruction qui n'encodent pas les dépendances dans les instructions, ou du moins pas explicitement. Voyons maintenant les architectures VLIW, EPIC et découplées. ==Les processeurs VLIW== Les processeurs VLIW et leurs dérivés font de l'émission multiple, à savoir exécuter plusieurs instructions indépendantes en parallèle, mais sans forcément avoir de l’exécution dans le désordre. Les '''processeurs VLIW''', ou ''very long instruction word'' exécutent des regroupements de plusieurs instructions qui s'exécutent en parallèle sur différentes unités de calcul. Les regroupements en question sont appelés des '''faisceaux d’instructions''' (aussi appelés ''bundle''). Le faisceau est chargé en une seule fois et est encodé comme une instruction unique. En clair, les processeurs VLIW chargent "plusieurs instructions à la fois" et les exécutent sur des unités de calcul séparées (les guillemets sont là pour vous faire comprendre que c'est en réalité plus compliqué). Une autre manière de voir les choses est que les faisceaux d'instruction regroupent plusieurs opérations en une seule super-instruction machine. Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. [[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]] ===L'attribution des instructions/opérations aux ALUs=== Sur un processeur VLIW pur, les faisceaux sont de taille fixe. Aussi, le regroupement des instructions est grandement facilité, de même que leur attribution aux unités de calcul. L'attriobution d'une instruction à une unité de calcul utilise l''''encodage par position'''. Avec cette méthode, un faisceau est découpé en créneaux (''slot''), chacun étant attribué à une ALU. La position de l'instruction dans le faisceau détermine l'ALU à utiliser. Les opérations/instructions ne peuvent pas être réparties n'importe comment dans un faisceau. Pour le dire autrement, chaque créneau ne peut contenir que quelques instructions bien précises,compatibles avec l'unité de calcul associée. Par exemple, le premier créneau ne peut accepter que des instructions d'addition, le second que des multiplications, le dernier que des instructions transcendantales, etc. {|class="wikitable" |- ! colspan="3" |Instruction VLIW à 3 slots |- | Slot 1 || Slot 1 || Slot 3 |- | Addition || Multiplication || Décalage à gauche |} En conséquence, il y a de nombreuses contraintes quand au regroupement des opérations/instructions. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant de deux additionneurs et d'un circuit multiplieur : il sera possible de regrouper deux additions avec une multiplication, mais pas deux multiplications ou trois additions. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc. ===L'exécution ''lockstep'' des opérations/instructions d'un faisceau=== Sur un processeur VLIW idéal, toutes les instructions d'un faisceau s'exécutent en même temps, durant les mêmes cycles d'horloge. La conséquence est que si une instruction/opération bloque le pipeline, toutes les instructions du faisceau doivent l'attendre. Par exemple, si un accès mémoire est dans un faisceau, les autres instructions s’exécutent en parallèle, mais on ne peut pas passer au faisceau suivant tant que l'accès mémoire n'est pas terminé. Un faisceau est un tout, qui s’exécute en bloc, on ne peut pas passer au faisceau suivant tant que tout le faisceau précédent est terminé. On parle parfois d''''exécution ''lockstep''''' d'un faisceau. Un défaut important est la gestion des instructions multi-cycle et notamment des accès mémoire. Lors d'un défaut de cache, les autres unités de calcul sont inutilisées une fois qu'elles ont fait les calculs associés dans le faisceau. Ce ne serait pas le cas avec un processeur à lectures non-bloquantes ou à exécution dans le désordre. Les instructions suivant la lecture pourraient parfaitement alimenter les unités de calcul, à condition d'être indépendants de la lecture. Mais avec un processeur VLIW, impossible de démarrer les opérations du faisceau suivant tant que l'accès mémoire n'est pas terminé. Idem avec les instructions multicycle, pour des instructions flottantes ou les multiplications/divisions. ===Les dépendances de données sont interdites dans un faisceau=== Les instructions/opérations d'un faisceau sont censées être indépendantes, dans le sens où elles n'ont pas de dépendances de données. Aussi, des difficultés arrivent quand des instructions/opérations d'un faisceau manipulent le même registre. Pour les lectures, cela ne pose pas de problème : la donnée lue est envoyée à plusieurs ALU, pas de quoi déclencher des problèmes, il faut juste que la connexion entre registres et unité de calcul le permettent, ce qui est souvent le cas. Les problèmes apparaissent avec des écritures. En théorie, deux instructions/opérations ne sont pas censées écrire dans le même registre. De même, si les instructions sont indépendantes, une instruction ne doit pas lire un registre écrit par une autre instruction. Mais dans les faits, cette situation arrive sur certains processeurs VLIW. Et ils peuvent gérer la situation de trois manières. La toute première est la plus simple : le processeur interdit à deux instructions d'un même faisceau d'avoir une dépendance de ce type. Si une instruction écrit dans un registre, tout autre instruction du faisceau ne peut pas lire ce registre. Une autre solution autorise ce genre de choses, mais avec une subtilité : la lecture du registre renvoie la valeur avant l'écriture. En clair, les deux instructions n'ont pas de dépendances entre elles, c’est juste que l'instruction lectrice lit une donnée qui est écrasée par la seconde au cycle suivant. L'implémentation est assez simple au niveau du matériel, et assez intuitive. Il existe cependant une exception, qui permet à plusieurs instructions d'écrire dans un registre. Il s'agit d'une sous-catégories d'instructions à prédicat sur le processeur Itanium. Les instructions en question effectuent une comparaison, puis font un ET/OU/XOR entre le résultat et un registre à prédicat. Or, l'Itanium peut exécuter plusieurs comparaisons de ce type en parallèle, en mettre plusieurs dans un seul faisceau. Si plusieurs comparaisons utilisent le même registre à prédicat et font un ET avec leur résultat, le résultat sera un ET entre tous les résultats des comparaisons. Idem avec un OU ou un XOR sur un même registre. Par contre, impossible de faire à la fois un OU et un ET sur le même registre. Il s'agit donc d'une exception à la règle : pas d'écritures simultanées dans le même registre. ===La gestion des exceptions matérielles avec des faisceaux d'instructions=== Passons maintenant aux dépendances de contrôle, qui existent encore sur les architectures VLIW. L'exécution en bloc, ''lockstep'', est censée résoudre ces dépendances pour ce qui est des branchements. Mais il ne faut pas oublier les exceptions matérielle. Une instruction/opération peut déclencher une exception, et il faut la traiter avec la granularité d'un faisceau. Un processeur VLIW peut gérer la situation de plusieurs manières différentes, qui dépendent du processeur. La plus simple invalide toutes les instructions du faisceau, l'exception est traitée au niveau du faisceau. Le faisceau est ré-executé intégralement une fois l'exception traitée par la routine adéquate. Une solution complétement à l'opposé exécute toutes les instructions du faisceau, sauf celle qui a levé l'exception. La première fait qu'on doit ré-exécuter toutes les instructions du faisceau, l'autre solution n'en ré-exécute qu'une seule. En terme de consommation énergétique et de performance, les deux sont complétement opposées : ré-exécution maximale couteuse en performance et énergie d'un côté, ré-exécution minimale et peu couteuse en énergie/performance de l'autre. Mais elles ont un point commun : dans les deux cas précédents, les exceptions deviennent alors imprécises. Une autre solution, plus compatible avec le modèle familier aux programmeurs, ajoute un support des exceptions précises. L'idée est d'invalider uniquement les instructions qui suivent l'exception dans l'ordre du programme. Pour cela, le compilateur doit ajouter quelques informations sur l'ordre des instructions dans le faisceau. Une solution intermédiaire invalide seulement les instructions qui ont une dépendance avec celle qui a levé l'exception. On a alors des exceptions semi-précises, mais les performances sont meilleures vu qu'on n'a pas à ré-exécuter beaucoup d'instructions. ===Le compilateur a un rôle primordial sur les architectures VLIW=== Les architectures VLIW ont des avantages certains : hardware très simple, émission multiple supportée nativement, etc. Mais elles ont aussi divers problèmes, comme une faible compatibilité, des performances limitées par les dépendances existantes à la compilation et une densité de code mauvaise. Tous les avantages et inconvénients majeurs ont la même source : c'est le compilateur qui regroupe plusieurs instructions/opérations en un seul faisceau. Le compilateur garantit que les instructions/opérations regroupées sont indépendantes et peuvent s’exécuter en parallèle. Un avantage à cela est que les processeurs VLIW ont un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction, pas besoin de hardware pour cela, pas besoin d'une unité d'émission complexe, ni d'exécution dans le désordre. Il y a juste besoin d'un ''scoreboard'' pour gérer les instructions multicycle et les accès mémoire. Alors qu'avec un processeur superscalaire, il y a des circuits de détection des dépendances entre instructions assez complexes et couteux en circuits. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance, ce qui fait que les cartes graphiques incorporent un ''scoreboard'' ou quelque chose de similaire. Mais pour ce qui est des désavantages, les processeurs VLIW ont une performance très dépendante du compilateur. Le compilateur doit analyser le code source pour détecter les dépendances d'instruction, et faire les regroupements. En soi, analyser les dépendances entre instruction est assez simple pour les compilateurs modernes, qui utilisent une représentation SSA. Mais malgré cela, les compilateurs ne font pas du très bon travail. Il arrive régulièrement que le compilateur ne puisse pas remplir tout le faisceau avec des instructions indépendantes. Sur les anciens processeurs VLIW, les instructions VLIW (les faisceaux) étaient de taille fixe, ce qui forçait le compilateur à remplir d'éventuels vides avec des NOP, diminuant la densité de code. La majorité des processeurs VLIW récents utilise des faisceaux de longueur variable, supprimant ces NOP. Mais il reste le fait que ces NOP sont une sous-exploitation des unités de calcul. De plus, certaines dépendances entre instructions ne peuvent être supprimées, ce qu'un processeur à exécution dans le désordre peut faire. Par exemple, le fait que les accès à la mémoire aient des durées variables (suivant que la donnée soit dans le cache ou la RAM, par exemple) joue sur les différentes dépendances. Un compilateur ne peut pas savoir combien de temps va mettre un accès mémoire et il ne peut organiser les instructions d'un programme en conséquence. Par contre, un processeur le peu. Autre exemple : les dépendances d'instructions dues aux branchements, qui ont tendance à limiter fortement les possibilités d'optimisation du compilateur. Autre défaut : les processeurs VLIW n'ont strictement aucune compatibilité, ou alors celle-ci est très limitée. En effet, le format des faisceaux VLIW est spécifique à un processeur. Celui-ci va dire : telle instruction va sur telle ALU, et pas ailleurs. Mais si on rajoute des unités de calcul dans une nouvelle version du processeur, il faudra recompiler notre programme pour que celui-ci puisse l'utiliser, voire simplement faire fonctionner notre programme. Dans des situations dans lesquelles on se moque de la compatibilité, cela ne pose aucun problème : par exemple, on utilise beaucoup les processeurs VLIW dans l'embarqué. Mais pour un ordinateur de bureau, c'est autre chose... ===Un petit historique des processeurs VLIW=== Les processeurs VLIW sont une idée assez ancienne, qui a ses sources dans un algorithme conçu à base pour l'implémentation d'un microcode horizontal ! Dans les années 80, Joseph Fisher travaillait sur l'architecture du CDC-6600, un super-ordinateur dont le processeur avait un microcode horizontal. Et le microcode horizontal a justement des ressemblances avec les processeurs VLIW. Frustré par les difficultés à coder un microcode sur ce genre de machines, il chercha un algorithme qui part d'une séquence de micro-opérations, et qui les regroupe dans des micro-opérations horizontales. L'algorithme qui naquit de ces recherches est appelé l''''algorithme de ''trace scheduling'''''. L'algorithme était capable de regrouper des opérations isolées dans un paquet encodé avec une seule micro-opération. Quelques architectures similaires au VLIW existaient déjà à l'époque, mais étaient prévues pour être codées en assembleur. L'invention de cet algorithme rendit ces architectures bien plus utiles. Le parallélisme exposé dans les micro-instructions horizontales est similaire à celui exposé par les architectures VLIW. Il était alors possible de prendre un compilateur, d'exécuter un algorithme de ''trace scheduling'' sur les instructions, et de se retrouver avec un programme encodant directement les des faisceaux VLIW. Fisher participa alors à un projet de recherche visant à créer un processeur VLIW, nommé le ELI-512 (''Extremely Long Instruction''-512), dont les instructions faisaient 512 bits et encodaient jusqu’à 30 instructions machines. Par la suite, il créa l'entreprise Multiflow, qui batit les premiers processeurs VLIW commerciaux, les bien nommés processeurs Multiflow. L'entreprise Cydrome tenta de leur faire concurrence. Mais les deux entreprises firent faillite au bout de quelques années. Quelques tentatives récentes de faire revenir ces architectures sur le devant de la scène ont été des échecs. Citons le cas de l'architecture Itanium d'Intel, ainsi que les processeurs Crusoe de l'entreprise Transmetta. Les deux visaient à remplacer le jeu d'instruction x86 des PC modernes, objectifs très compliqué et sans doute voué à l'échec du fait des problèmes de compatibilité intrinsèques. Transmetta a pourtant pris le problème à bras le corps, avec des techniques de traduction x86-VLIW très performantes, mais purement logicielles. Mais rien à faire, les processeurs VLIW n'ont percé que dans des domaines où la compatibilité avec le code existant n'est pas nécessaire, l'embarqué étant le meilleur d'entre eux. Les processeurs VLIW ont été autrefois utilisés dans les cartes graphiques AMD et possiblement NVIDIA de l'époque de DirextX 8/9. Mais tout cela est le sujet d'un autre wikilivre... ==Les instructions multicyles et le pipeline sur les CPU VLIW== La discussion précédente partait du principe que le processeur VLIW n'avait pas de pipeline. L'implémentation du VLIW sur un pipeline demande cependant quelques modifications. Le cas le plus simple est celui d'un pipeline de longueur fixe, où toutes les opérations s'exécutent en un seul cycle d'horloge. Sans système de contournement, l'usage du pîpeline fait qu'il y a un délai entre le moment où une opération lit ses opérandes dans les registres et celui où son résultat est enregistré dans les registres. Quelques cycles d'horloges, tout au plus, mais il s'agit d'un délai qui fait que le résultat d'une opération est enregistré en retard. Or, un processeur pipeliné démarre un nouveau faisceau à chaque cycle d'horloge. Ce qui pose des problèmes si deux faisceaux ont des dépendances de données. Si on lance ces deux faisceaux l'un à la suite de l'autre, le second faisceau ne lira pas le résultat calculé par le premier. On retombe sur les problèmes liés à l'émission dans l'ordre/désordre, que les architectures VLIW souhaitaient éviter. ===Les mitigations et solutions=== Il est possible de mitiger le tout dans le cas d'un pipeline de longueur fixe, où toutes les instructions s'exécutent en un cycle d'horloge. Pour cela, il suffit d'utiliser un système de contournement, pour envoyer les résultats calculés par l'ALU sur son entrée, afin de mieux gérer les dépendances de données de type RAW. Sans cela, deux faisceaux avec une dépendance ne peuvent pas être démarrés l'un après l'autre. Mais la solution ne résout le problème que si toutes les instructions/opérations s'exécutent en cycle d'horloge au niveau de l'ALU. Dans les faits, la solution ne marche pas pour les instructions multicycles, dont les accès mémoire. Une solution à cela serait de vérifier les dépendances entre faisceaux avec un ''scoreboard'' ou un circuit similaire. En cas de dépendances entre les faisceaux, l'exécution ''lockstep'' des instructions fait qu'on doit ajouter des bulles de pipeline. Le problème est que l'on ne profite pas du pipeline, sauf à utiliser un système de contournement complexe. De plus, cela demanderait d'améliorer l'unité d'émission et d'ajouter du hardware pour cela, ce qui colle mal à la philosophie du VLIW. Mais c'est l'une des seule solution possible pour gérer les accès mémoire. Une autre solution est de laisser le travail au compilateur, qui gère de lui-même les latences des instructions liées au pipeline. Le compilateur doit donc produire un code machine spécifique à un processeur, qui prend en compte la longueur du pipeline. La solution marche pas trop mal pour les instructions multicycles exécutées dans une ALU, mais pas trop pour les accès mémoire. Pour les instructions multicycles, le compilateur connait la latence de chaque opération et s’arrange pour que le code compilé n'aie pas de problèmes. Par exemple, si une unité de calcul multicycle est utilisée pendant 5 cycles, elle s'arrange pour que les 5 faisceaux suivants ne l'utilisent pas. Le compilateur doit gérer les latences pour chaque opération d'un faisceau, vérifier que des faisceaux consécutifs soient compatibles, et ainsi de suite. ===Les modèles de latence d'instruction=== Peu importe que l'on utilise un ''scoreboard'' ou le compilateur, la latence des instruction a un grand impact dans la manière dont on exécute/compile le code. Le processeur connait la latence maximale d'une instruction multicycle, sauf éventuellement pour les accès mémoire. Idéalement, le compilateur fonctionne mieux avec des latences fixes, mais il peut avoir à se débrouiller avec des latences variables. Les deux cas donnent des résultats très différents pour le compilateur, et ils sont deux modèles différents, qui doivent être pris en compte à la création du processeur. Avec le premier modèle, la latence des instructions est fixe, à savoir qu'elles prennent toujours le même nombre de cycles pour s'exécuter. Si une instruction de multiplication prend 5 cycles, elle prendra toujours 5 cycles, pas un de moins. Avec ce modèle, le compilateur a plus de facilités à gérer les registres. Par contre, la gestion des exceptions précises est particulièrement complexe. Le processeur doit être conçu pour. Il se peut que l'instruction finisse avant dans l'unité de calcul, mais l'écriture dans les registres sera alors retardée pour respecter la latence fixée. Le chemin de données du processeur doit être conçu en conséquence et doit mettre en attente les résultats disponibles à l'avance. Le second modèle permet à une instruction multicycle de finir plus tôt que prévu, leur latence est variable. Par exemple, une instruction de multiplication a une latence maximale de 5 cycles, mais il se peut qu'elle prennent 4/3 cycles si les opérandes sont particulières. Le compilateur a un peu plus de mal avec ce genre de modèle. Mais l'implémentation des exceptions précises est bien plus simple. De plus, cela améliore un petit peu la compatibilité dans certains cas. Si le nouveau modèle du processeur a une latence inférieure pour certaines instructions, le code conçu pour l'ancien processeur' marchera toujours, et ses performances seront améliorées. ==Les processeurs VLIW à instruction de longueur variable== Les processeurs VLIW purs ont de nombreux défauts, mais cela ne signifie pas qu'ils n'ont pas de solutions. Cependant, ces solutions sont un peu opposées à la philosophie de base des processeurs VLIW : réduire au maximum le hardware lié au décodage et aux unités d'émission, tout an gardant l'émission multiple. Les solutions en question sont multiples, mais elle partent du même principe : elle encodent des faisceaux de taille variable. Par taille variable, on veut dire que les faisceaux ont un nombre d'instructions/opérations variables, qui peut varier d'un faisceau à l'autre. Par exemple, prenons un processeur VLIW codant ses faisceaux sur une taille fixe de 512 bits. En passant à des faisceaux de taille variable, les faisceaux peuvent faire entre 64 et 512 bits, par exemple. Pour cela, les NOPs, les instructions qui ne font rien, sont retirées du faisceau ou sont encodées de manière compressée. Et cela implique que le décodage des instructions et leur émission est plus complexe, elle demande plus de circuits. La densité de code est grandement améliorée, de même que la compatibilité. Les faisceaux ont donc une taille variable, comprise entre une taille minimale et une taille maximale. La taille maximale est obtenue quand aucun NOP n'est présent dans le faisceau. Par contre, le processeur doit pouvoir charger un faisceau complet. Par exemple, pour un processeur dont les faisceaux font entre 64 et 512 bits, le processeur charge 512 bits en une fois, en un accès au cache d'instruction. Il faut donc faire la distinction entre les faisceaux et les '''paquets de chargement'''. Un paquet de chargement dans l'exemple précédent fait 512 bits, mais les faisceaux en font entre 64 et 512. L'avantage principal est uen amélioration de la densité de code : pas besoin d'insérer des NOPs à l'intérieur des faisceaux, même si on peut avoir besoin d'insérer des NOPs de bourrage. Avec un faisceau de taille variable, il n'y a pas besoin d'ajouter des NOPs si le faisceau est partiellement remplit, on a juste à raccourcir le faisceau. Le défaut est que le décodage et le chargement des instructions est plus complexe. Les faisceaux n'ayant plus une taille fixe, il faut utiliser les techniques de chargement/décodage des instructions variables vues dans le chapitre sur l'unité de chargement. Cela fait du hardware en plus, un cout en performance et en énergie. Tout ce que la philosophie VLIW voulait éviter. En contrepartie, le gain en densité de code est important et le gain de compatibilité binaire est très important. ===La délimitation des faisceaux=== Un paquet de chargement peut contenir plusieurs faisceaux. Lorsqu'un paquet est chargé, il est découpé en faisceaux au fur et à mesure de l'exécution. Le processeur exécute les faisceaux les uns après les autres. Par exemple, si toutes les instructions d'un paquet doivent s’exécuter en série, il les exécute une après l'autre, et ne charge le paquet suivant qu'une fois que toutes les instructions du paquet sont terminées. Reste que pour cela, il faut trouver un moyen pour découper un paquet de chargement en plusieurs faisceaux. Pour cela, il existe plusieurs techniques, avec d'autres techniques dérivées. La première technique ajoute des '''méta-données''' au début du faisceau, à savoir quelques bits qui fournissent des informations sur le faisceau. Et parmi ces information, on peut intégrer la taille du faisceau, sa longueur en nombre d'octets ou d'instructions. Le processeur a juste à extraire la longueur du faisceau et sait comment découper le paquet de chargement en faisceaux. Les faisceaux sont donc placés à la suite les uns des autres en mémoire, mais sont précédés par un octet de méta-données qui indique la longueur du faisceau, comment sont organisées les instructions/opérations dedans, etc. Un défaut est que la taille d'un faisceau est limitée par le nombre de bits utilisés pour encoder les méta-données. La méthode peut être étendue pour gérer autre chose que la taille des faisceaux, comme on le verra plus bas. En effet, les méta-données ne se bornent pas à donner la longueur du faisceau, mais peuvent indiquer comment sont organisées les instructions dans le faisceau, ou toute autre information utile. A défaut d'intégrer la longueur du faisceau dans l'instruction, on peut intégrer des informations qui permettent de la calculer. Par exemple, nous verrons plus bas la technique du masque de NOPs qui permet de calculer la longueur de l'instruction sur la base d'informations utilisées pour autre chose. Les deux techniques suivantes dispersent des méta-données entre chaque instruction, au lieu de les regrouper dans un octet au début du faisceau. Pour ce faire, elles ajoutent des bits à la fin de chaque instruction, qui précisent s'il s'agit de la dernière instruction d'un faisceau. La solution est plus flexible, car les faisceaux peuvent avoir des tailles arbitraires, en théorie. Avec la première technique, chaque instruction est suivie d'un '''bit d’arrêt''' (stop bits) qui indique la fin d'un faisceau. [[File:Bits d’arrêt.png|centre|vignette|upright=2|Bits d’arrêt.]] Avec la seconde méthode, chaque instruction/opération est suivie d'un un '''bit de parallélisme''' (''parallel bit''), un bit placé à la fin d'une instruction qui dit si elle peut s'effectuer ou non en parallèle de la suivante. [[File:Bit de parallélisme.png|centre|vignette|upright=2|Bit de parallélisme.]] Un exemple est celui des processeurs Texas Instrument de série C6X, notamment le TMS320C62 (C62) et le TMS320C67 (C67). Sur ces processeurs, un paquet de chargement fait 256 bits et les paquets sont eux-même alignés en mémoire sur 256 bits. Les instructions font 32 bits, ce qui fait qu'on peut en mettre 8 par paquet de chargement. Les faisceaux sont indiqués avec des bits de parallélisme. ===L'attribution des instructions/opérations aux ALUs=== Le passage à des faisceaux de taille variable fait que l'on perd la correspondance unique entre une instruction/opération et une unité de calcul. Le fait que les NOPs sont devenus implicites fait que l'on doit répartir les opérations sur les différentes unités de calcul. Il faut déterminer quelle instruction/opération va sur tel unité de calcul. Les solutions pour cela sont assez variables, allant d'un codage des NOPs compressé avec un encodage explicite des NOPs, à des systèmes avec un encodage implicite. Une solution est de se débarrasser de l'encodage par position utilisé plus haut, où chaque instruction était attribuée à une ALU en fonction de sa place dans le faisceau. A la palce, on le remplace par l''''encodage par nommage''', où chaque instruction d'un faisceau précise l'unité de calcul qui doit la prendre en charge. L'instruction contient un numéro qui indique l'unité de calcul à utiliser. Cette technique est déclinée en deux formes : soit on trouve un identifiant d'ALU par instruction, soit on utilise un identifiant pour tout le faisceau, qui permet à lui seul de déterminer l'unité associée à chaque instruction. Une autre solution conserve l'encodage par position vu précédemment, mais représente les NOPS sous forme compressée. Les instructions/opérations sont donc toujours à la bonne place, à la bonne position dans le faisceau, mais les NOPs sont compressés et réduits à une taille plus petite. Le décodage doit alors juste identifier les NOPs et les étendre, de manière à retrouver le faisceau d'instruction non-compressé. Une solution pour cela est d'incorporer, dans le faisceau, un masque qui indique où sont les NOPs dans le faisceau. La technique porte le nom de '''masque de NOP'''. Prenons l'exemple d'un faisceau contenant 8 instructions/opérations maximum. Si j'ai par exemple 6 positions remplies avec deux NOPs, le faisceau contiendra 6 instructions seulement, les NOPs ne seront pas placés dans le faisceau. Par contre, le masque indiquera où sont les NOPs dans le faisceau, où il faut les placer. Le masque contient un bit par instruction, par ''slot'', par position dans le faisceau. Le premier bit est pour la première instruction, le second bit pour la seconde instruction, etc. Le bit est mis à 1 si l'instruction est un NOP, 0 sinon. Les NOPs ne sont donc pas représentés. {|class="wikitable" |- ! colspan="8" | Instruction décompréssée | ADD || MUL || class="f_jaune" | NOP || SUB || class="f_jaune" | NOP || class="f_jaune" | NOP || class="f_jaune" | NOP || LOAD |- ! colspan="8" | Masque de NOP | 0 || 0 || 1 || 0 || 1 || 1 || 1 || 0 |- ! colspan="8" | Instruction compressée | ADD || MUL || SUB || LOAD || 0010 1110 || colspan="3" | |} Le masque est généralement placé au tout début du faisceau, pour faciliter le décodage. Il faut noter que cette technique permet de compresser les faisceaux qui contiennent au moins un NOP. Mais les autres sont allongés d'un octet pour stocker le masque. La technique est donc d'autant plus efficace que le code contient de NOPs, ce qui fait qu'elle est surtout utile sur les CPU VLIW avec beaucoup de ''slots'', qui ont beaucoup d’instructions par faisceau, qui ont plus de difficultés à remplir leurs faisceaux. Il faut préciser qu'avec cette technique, l'encodage de la longueur de l'instruction n'est pas nécessaire. Déterminer la longueur de l'instruction se fait simplement à partir du masque. Il suffit de l'envoyer en opérande d'un encodeur, qui renvoie la longueur du faisceau. La solution a pour avantage de ne pas ajouter d'informations en plus dans le faisceau. Pour implémenter la technique, l'instruction est décompressée lors du chargement de l'instruction depuis le cache L1. Un circuit prend en entrée un paquet de chargement, et l'analyse, puis décompresse le faisceau en ajoutant les NOPs au bon endroit. L'avantage est que les instructions sont compressées dans le cache, ce qui utilise au mieux sa capacité. Une autre solution déplace le circuit de décompression avant le cache. Les faisceaux sont alors décompressés lors de leur chargement dans le cache d’instruction L1. ===L'alignement des faisceaux dans un paquet de chargement=== Un point important est que les faisceaux peuvent ou non être à cheval sur deux paquets de chargement. Certains processeurs ne le permettent pas. C'est le cas sur les processeurs Texas Instrument de série C6X, où un faisceau doit rentrer intégralement dans le paquet. Ainsi, le bit de parallélisme est à 0 toutes les 8 instructions, vu qu'un paquet de chargement fait 8 instructions. Un inconvénient est que cela peut parfois laisser des espaces vides à la fin d'un paquet d'instruction. Le compilateur essaye de faire rentrer le plus de faisceaux possibles dans un paquet de chargement, mais il arrive malgré tout qu'il reste de la place. Par exemple, pour un paquet de 8 instructions, imaginons qu'on ait un faisceau de 4 instructions, un autre de 3, et un autre de deux. Les deux premiers faisceaux rentrent dans le paquet, mais pas le troisième. Il manquera une instruction pour compléter, le vide est comblé par une instruction NOP qui ne fait rien. Le ou les NOP en question, sont appelés des '''NOP de bourrage'''. La compatibilité est très mauvaise avec les bits de bourrage, vu que cela limite la taille du paquet de chargement. De plus, cela réduit les opportunités de compression. On gagne en densité de code si et seulement si on peut mettre plusieurs faisceaux dans un seul paquet de chargement. L'idéal est d'utiliser des paquets de chargement assez grands, pour pouvoir mettre pleins de petits faisceaux dedans. Mais d'autres processeurs autorisent un système différent, où un faisceau peut être à cheval sur un paquet de chargement. Le chargement se fait paquet de chargement par paquet de chargement, avec un découpage des faisceaux lors du décodage. Les techniques utilisées pour cela sont similaires à celles utilisées pour le chargement des instructions de longueur variable. L'avantage est que cela permet de ne pas avoir à ajouter des NOPs de bourrage. Mais le cout en circuits est conséquent. La technique est notamment utilisée sur l'Itanium, comme on le verra plus bas. Les techniques pour charger des faisceaux non-alignées demandent d'accumuler les paquets de chargement dans une mémoire temporaire, et de le insérer au bon endroit, à la suite des faisceaux déjà chargés. Le tout implique des circuits de décalage et autres. Et pour qu'ils fonctionnent, ils doivent connaitre la longueur d'un faisceau, afin de décaler du bon nombre de rangs/instructions. Déterminer la longueur d'un faisceau dépend grandement de la méthode employée pour délimiter les faisceaux. Avec un octet de méta-donnée, la longueur peut être intégrée directement dans les méta-données du faisceau et n'a pas à être déterminée. Avec l'usage de bits d'arrêt ou de parallélisme, on peut la calculer sur la base de ces bits. Il suffit d'envoyer ces bits dans un circuit encodeur qui renvoie la longueur du faisceau, exprimée en nombre d'instructions. Avec un masque pour encoder les NOPs, l'encodage de la longueur se détermine à partir du masque de NOP. Là encore, il suffit de le faire passer dans un circuit qui prend le masque de NOP et détermine la longueur du faisceau. Le circuit n'est ni plus ni moins qu'un circuit de ''population count''. ===L'encodage des faisceaux sur l'Itanium=== L'Itanium utilise un mélange des deux méthodes. Elle regroupe les instructions dans des paquets de trois instructions appelés des ''bundles''. N'allez pas croire que ces ''bundles'' sont des paquets de chargement : les paquets de chargement contiennent 2 ''bundles'', voire plus. Les ''bundles'' ne peuvent pas regrouper n'importe quel triplet d'instructions, mais seulement certains regroupements prévus à l'avance. Encore une fois, il y a des contraintes sur les regroupements d'instructions dans un ''bundle''. Il y a en tout 32 possibilités, qui définissent chacune un mix d'instructions mémoire, entières, flottantes et de branchement. Les trois instructions d'un ''bundle'' sont associées à un '''octet de ''template''''', qui encode les dépendances entre instructions et qui dit comment découper le paquet de chargement en faisceaux. Il regroupe notamment les trois bits d'arrêt des trois instructions, et 5 bits annexes qui encodent la nature des instructions présentes dans le paquet. J'ai dit plus haut qu'il y a 32 possibilités pour les regroupements possibles, et bien on peut coder les 32 possibilités sur 5 bits. Le tout est codé sur 128 bits, avec 40 bits pour chaque instruction, plus l'octet de ''template''. [[File:Encodage des stop bits sur l'Itanium.png|centre|vignette|upright=3|Encodage des stop bits sur l'Itanium]] Un faisceau peut être intégralement contenu dans un ''bundle'' s'il fait trois instructions ou moins. Dans le cas contraire, il est répartit sur plusieurs ''bundles'' et c'est au processeur de reconstituer le faisceau à partir de plusieurs ''bundles''. L'usage de bits d'arrêt permet à un faisceau d'être à cheval sur deux ''bundles'' et éventuellement sur deux paquets de chargement. Les paquets de chargement chargent 2 ''bundles'' à la fois sur l'Itanium 1 et 2. De plus, les paquets de chargement sont accumulées dans un tampon de 8 ''bundles'' (24 instructions). Le tampon s'assure que deux ''bundles'' soient disponibles pour les unités de décodages, vu que le processeur a la capacité d'exécuter 2 ''bundles'' à la fois dans ses unités de calcul. L'unité de décodage se charge de reconstituer des faisceaux d'instruction en utilisant les octets de ''template'' des ''bundles'' chargés, en analysant les bits d'arrêts et les bits qui encodent le regroupement des instructions. Avec la technique utilisée sur l'Itanium, la compatibilité binaire est très bonne. Il est possible de changer les unités de calcul du processeur sans que la compatibilité binaire soit altérée. D'ailleurs, l'Itanium n'a lui-même pas de correspondance entre un ''bundle'' et ses unités de calcul, vu qu'il a 6 unités de calcul/mémoire, pour des ''bundles'' de trois instructions. Il pourrait en rajouter ou en enlever sans que ce soit un problème, il faudrait juste adapter le décodeur. ==Les processeurs hybrides VLIW/RISC== Il existe des processeurs hybrides entre processeurs RISC et VLIW. L'idée est que ces processeurs gérent à la fois des instructions machines isolées, et des faisceaux d'instructions. Il en existe assez peu, mais il est globalement possible de quand même les classer en deux sous-types. Le premier est celui où il est possible de mélanger instructions RISC et VLIW sans véritables contraintes. L'autre est celui où le processeur a deux modes de fonctionnement :un mode RISC, et un mode VLIW, les deux étant totalement séparés et ne pouvant pas être mixés. ===Les CPU RISC/VLIW à instructions mixées=== L'encodage des instructions autorise des instructions isolées, parfois complétées par des faisceaux d'instructions à l'encodage spécifique. Le processeur exécute un programme qui mélange instructions normales et faisceaux VLIW librement. Un exemple est celui des processeurs Xtensa LX2 de Tensilica. Ils appelent cette technique sous le nom de ''Flexible Length Instruction eXtensions'' (FLIX). Le nom trahit bien que les instructions VLIW sont une extension d'un jeu d'instruction normal, au même titre que les extensions MMX/SSE/x87 et autres du jeu d'instruction x86 s'ajoutent aux instructions x86 normales. Les instructions normales font entre 16 et 24 bits, alors que les faisceaux font soit 32, soit 64 bits. Ils peuvent donc regrouper 2 à 4 instructions normales. Les processeurs DSP Infineon Carmel utilise une technique similaire. Ils sont des CPU RISC dont les instructions de base font 48 bits. Ils disposent aussi d'instructions courtes codées sur 24 bits. Et enfin, l'extension ''Configurable Long Instruction Word'' (''CLIW'') permet de gérer des faisceaux VLIW qui regroupent six instructions machines. Vu que le processeur charge 48 bits d'un seul coup, il peut exécuter une à deux instructions courtes d'un seul coup, ce qui en fait une forme basique de VLIW, complémentaire du CLIW. ===Les CPU RISC/VLIW à deux modes de fonctionnement=== Le processeur Intel i860 utilise une méthode différente. Le processeur peut fonctionner en mode RISC normal, ou en mode VLIW. En mode RISC, il peut exécuter des instructions entières ou flottante, les deux étant codées sur 32 bits. En mode VLIW, il exécute des faisceaux VLIW codés sur 64 bits. L'implémentation du VLIW profite de la présence d'une unité FPU séparée de l'ALU entière. En mode VLIW, les faisceaux d'instruction sont composés en concaténant une instruction entière et une instruction flottante, le tout faisant 64 bits. Les faisceaux sont alignées sur 64 bits, la première instruction est forcément entière, la seconde est une instruction flottante. L'instruction flottante peut faire une multiplication et une addition à la suite, vu qu'on a un additionneur flottant relié à un multiplieur flottant. Les deux circuits ont une interconnexion partiellement programmable, afin de faciliter l'implémentation de certaines instructions flottantes. ==Les processeurs EPIC== En 1997, Intel et HP lancèrent un nouveau processeur, l'Itanium, dont l'architecture corrigeait les défauts des autres processeurs VLIW. Dans un but marketing évident, Intel et HP prétendirent que l'architecture de ce processeur n'était pas du VLIW amélioré, mais une nouvelle architecture appelée ''EPIC'', pour '''''Explicit Parallelism Instruction Computing'''''. Il faut avouer que cette architecture avait de fortes différences avec le VLIW, mais aussi beaucoup de points communs. Bien évidemment, beaucoup ne furent pas dupes, et une gigantesque controverse vit le jour : est-ce que les architectures EPIC sont des VLIW ou non ? Dans cette section, nous allons voir les optimisations vraiment spécifiques des processeurs Itanium. Il implémente des faisceaux d'instruction de taille variable, mais aussi des techniques de spéculation avancées. Les techniques spécifiques aux processeurs Itanium sont des techniques de spéculation avancées, qui permettent de gérer les exceptions ou les branchements, ou des accès mémoire. Vu qu'il s'agit de techniques spéculatives, on s'attend à ce que ce soit le processeur qui soit en charge de ces spéculations, mais la réalité est qu'elles sont une collaboration entre CPU et compilateur ! ===Les exceptions différées=== L'Itanium implémente ce qu'on appelle les '''exceptions différées'''. Avec cette technique, le compilateur crée deux versions d'un même code : une version optimisée qui suppose qu'aucune exception matérielle n'est levée, et une autre version qui prend en compte les exceptions. Le programme exécute d'abord la version optimisée de manière spéculative, mais annule son exécution et repasse sur la version non-optimisée en cas d'exception. Pour vérifier l’occurrence d'une exception, chaque registre est associé à un bit « rien du tout » (''not a thing''), mis à 1 s'il contient une valeur invalide. Si une instruction lève une exception, elle écrira un résultat faux dans un registre et le bit « rien du tout » est mis à 1. Les autres instructions propageront ce bit « rien du tout » dans leurs résultats : si un opérande est « rien du tout », le résultat de l'instruction l'est aussi. Une fois le code fini, il suffit d'utiliser une instruction qui teste l'ensemble des bits « rien du tout » et agit en conséquence. ===La spéculation sur les lectures=== L'Itanium fournit une fonctionnalité similaire pour les lectures, où le code est compilé dans une version optimisée où les lectures sont déplacées sans tenir compte des dépendances, avec un code de secours non-optimisé. Encore une fois, le processeur vérifie si la spéculation s'est bien passée, avant de décider de passer sur le code de secours ou non. La vérification ne se fait pas de la même façon selon que la lecture ait été déplacée avant un branchement ou avant une autre écriture. * Si on passe une lecture avant un branchement, la lecture et la vérification sont effectuées par les instructions LD.S et CHK.S. Si une dépendance est violée, le processeur lève une exception différée : le bit « rien du tout » du registre contenant la donnée lue est alors mis à 1. CHK.S ne fait rien d'autre que vérifier ce bit. * Si on passe une lecture avant une écriture, la désambiguïsation de la mémoire est gérée par le compilateur. Tout se passe comme avec les branchements, à part que les instructions sont nommées LD.A et CHK.A. [[File:Spéculation sur les lectures.png|centre|vignette|upright=2|Spéculation sur les lectures.]] Pour détecter les violations de dépendance, le processeur maintient une liste des lectures spéculatives qui n'ont pas causé de dépendances mémoire, dans un cache : la '''table des adresses lues en avance''' (''advanced load address table'' ou ALAT). Ce cache stocke l'adresse, la longueur de la donnée lue, le registre de destination, etc. Toute écriture vérifie si une lecture à la même adresse est présente dans l'ALAT : si c'est le cas, une dépendance a été violée, et la lecture est retirée de l'ALAT. ===Les bancs de registres tournants=== Les processeurs EPIC et VLIW utilisent une forme limitée de renommage de registres pour accélérer certaines boucles. Pour l’expliquer, prenons une boucle simple et intéressons-nous au corps de la boucle, à savoir la boucle sans les branchements et instructions de test qui servent à répéter les instructions. La boucle d'exemple se contente d'ajouter 5 à tous les éléments d'un tableau. L'adresse de l’élément du tableau est stockée dans le registre R2. Dans le code qui suivra, les crochets serviront à indiquer l'utilisation du mode d'adressage indirect. Sans optimisations, le corps de la boucle est le suivant : loop : load R5 [R2] / add 4 R2 ; add 5 R5 ; store [R2] R5 ; Les différentes itérations de la boucle peuvent se calculer en parallèle, vu que les éléments du tableau sont manipulés indépendamment. Mais codée comme dessus, ce n'est pas possible car les trois instructions de la boucle utilisent le registre R5 et ont donc des dépendances. Le renommage de registres peut éliminer ces dépendances, mais il n'est pas disponible sur les processeurs VLIW et EPIC. À la place, les concepteurs de processeurs ont inventé les '''bancs de registres tournants''' (''rotating register files''). Avec cette méthode, la correspondance (nom de registre - registre physique) se décale d'un cran à chaque cycle d’horloge. Par exemple, le registre nommé R0 à un instant donné devient le registre R1 au cycle d'après, et idem pour tous les registres. Précisons que sur l'Itanium, cette technique est appliquée non pas à l'ensemble du banc de registre, mais est limitée à un banc de registres spécialisé dans l’exécution des boucles. Évidemment, le code source du programme doit être modifié pour en tenir compte. Ainsi, le code vu précédemment devient celui-ci. loop : load RB5 [R2] / add 4 R2 ; add 5 RB6 ; store [R2] RB7 ; Ainsi, le LOAD d'une itération ne touchera pas le même registre que le LOAD de l'itération suivante, idem pour l'instruction de calcul et le STORE. Le nom de registre sera le même, mais le fait que les noms de registre se décalent à chaque cycle d'horloge fera que ces noms identiques correspondent à des registres différents. Les dépendances sont supprimées, et le pipeline est utilisé à pleine puissance. Cette technique s'implémente avec un simple compteur, incrémenté à chaque cycle d'horloge, qui mémorise le décalage à appliquer aux noms de registre. À chaque utilisation d'un registre, le contenu de ce compteur est ajouté au nom de registre à accéder. ==Les architectures découplées== Les '''architectures découplées''', aussi appelées '''''Decoupled Access/ExecuteComputer Architectures''''', sont totalement différentes des architectures VLIW. Elles sont bien des architectures à parallélisme d'instruction explicites, mais leur rôle est d'implémenter une forme limitée d’exécution dans le désordre, pas d’exécuter des instructions indépendantes en parallèles. Par contre, elle ont pour point commun avec les architectures VLIW : elles n'encodent pas les dépendances entre instructions de manière explicite dans les instructions, ce que font les architectures ''dataflow''. Les architectures découplées séparent les accès mémoires des autres instructions dans deux programmes qui s’exécutent en parallèle, ce qui permet une forme limitée d’exécution dans le désordre. Le découpage du programme en plusieurs flux s'effectue à la compilation. Les deux flux sont chacun exécutés sur un processeur séparé, un le processeur ''access'' en charge des accès mémoire et un processeur ''execute'' pour les calculs et des branchements. Le transfert des opérandes entre processeurs se fait par l'intermédiaire de mémoires tampons de type FIFOs, manipulées par le programmeur. Les transferts se font dans les deux sens : du processeur ''access'' vers le processeur ''execute'' pour les lectures, dans l'autre sens pour les écritures. Les branchements demandent cependant une synchronisation entre les deux flux d'instruction, afin de garantir qu'un branchement s’exécute bien au même moment dans les deux flux. Et toute la difficulté est de synchroniser les deux processeurs. Cette technique n'est pas compatible avec l’exécution dans le désordre telle que vue dans les chapitres précédents, mais elle en reprend certains avantages. L'avantage principal est que la séparation en deux flux permet de profiter d'une forme limitée d’exécution dans le désordre, mais avec beaucoup moins de structures matérielles, moins de registres, moins de circuits. Les deux flux étant séparés, l'un peut continuer à s’exécuter alors que l'autre est en train d'attendre. Par exemple, le flux de calcul peut continuer à faire des calculs pendant que l'autre est coincé dans un accès mémoire de longue durée. Ou alors, le flux d'accès mémoire peut charger des données à l'avance, pendant que le flux de calcul est bloqué dans un calcul long (une division, par exemple). Cette forme de parallélisme est certes plus limitée que celle permise par l’exécution dans le désordre, mais le principe est là. : Dans ce qui suit, le processeur ''access'' sera noté processeur A, et le processeur ''execute'' sera noté processeur X, pour plus de simplicité. Pour en savoir, voici quelques liens utiles. * [https://course.ece.cmu.edu/~ece740/f13/lib/exe/fetch.php?media=p289-smith.pdf ''Decoupled Access/Execute Computer Architectures''] ===Les échanges de données entre processeurs=== Prenons le cas d'une lecture : la lecture est démarrée par le processeur A, puis est transmise au processeur X. La transmission au processeur X se fait par l'intermédiaire d'une mémoire FIFO, la file de lecture de X (''X load queue'', abréviée XLQ). Quand le processeur X a besoin d'un opérande lu depuis la mémoire, il vérifie si elle est présente dans l'XLQ et se met en attente tant que ce n'est pas le cas. Pour les écritures, l'adresse de l'écriture est calculée par le processeur A, tandis que la donnée à écrire est calculée par le processeur X, et les deux ne sont pas forcément disponibles en même temps. Pour cela, les adresses et les données sont mises en attente dans deux mémoires tampons qui coopèrent entre elles. La première est intégrée dans le processeur A et met en attente les adresses calculées : c'est la file d’écriture des adresses (''store address queue'', ou SAQ). La seconde met en attente les données à lire, et relie le processeur X au processeur A : c'est la file d’écriture de X (''X store queue'', ou XSQ). Quand l'adresse et la donnée sont disponibles en même temps, l'écriture est envoyée à la mémoire ou au cache. Les échanges entre processeurs peuvent aussi se faire sans passer par des files de lecture/écriture. Cela peut servir pour diverses scénarios. Par exemple, le calcul d'une adresse sur le processeur A peut utiliser des données calculées par le processeur X. Pour cela, les échanges s'effectuent par copies inter-processeurs de registres. Un registre peut être copié du processeur A vers le processeur X, ou réciproquement. Pour utiliser cette unité de copie, chaque processeur dispose d'une instruction COPY. ===Les branchements=== Les branchements sont présents à des endroits identiques dans les deux flux, ce qui fait que les structures de contrôle présentes dans un programme sont présentes à l'identique dans l'autre, etc. Or, les branchements doivent donner le même résultat dans les deux processeurs. Pour cela, le résultat d'un branchement sur un processeur doit être transmis à l'autre processeur. Pour cela, on trouve deux files d'attente : * la file A vers X (''access-execute queue'', abréviée AEQ), pour les transferts du processeur A vers le processeur X ; * la file X vers A (''execute-access queue'', abréviée EAQ), pour l'autre sens. Il faut noter que le processeur peut se trouver définitivement bloqué si l'AEQ et l'EAQ sont toutes deux totalement remplies ou totalement vide. Pour éviter cette situation, les compilateurs doivent compiler le code source d'une certaine manière. [[File:752a78c7-71e0-4580-afdd-538Résumé d'une architecture découplée de type access-execute.e30998305.png|centre|vignette|upright=2|Résumé d'une architecture découplée de type access-execute.]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Exemples de microarchitectures CPU : le cas du x86 | prevText=Exemples de microarchitectures CPU : le cas du x86 | next=Les architectures dataflow | nextText=Les architectures dataflow }} </noinclude> s0k6dacaahn5dt5rns0jiq948ph0ozx Mathc complexes/a26 0 79758 745988 745460 2025-07-05T14:43:02Z Xhungab 23827 745988 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] : [[Mathc complexes/h08a| '''Propriétés et Applications''']] : : '''L'étude de ce chapitre peut ce faire à l'aide de cette [https://youtube.com/playlist?list=PLi6peGpf8EPMnU50SHdNTVMVm0C32g5ZX Playlist]..''' : . : {{Partie{{{type|}}}| '''Total Pivoting''' }} : {{Partie{{{type|}}}| Résoudre un système d'équations :}} : {{Partie{{{type|}}}|[[Mathc_complexes/a27| Gauss-Jordan]]}} : {{Partie{{{type|}}}|[[Mathc complexes/a302| L'inverse]]}} : . : {{Partie{{{type|}}}| Application : '''Total Pivoting''' }} : {{Partie{{{type|}}}|[[Mathc complexes/053| Analyse d'un '''réseau''' dans '''R''']]}} : {{Partie{{{type|}}}|[[Mathc complexes/05a| Analyse d'un circuit '''électrique''' dans '''R''']]}} : . : {{Partie{{{type|}}}|Applications : '''Géometrie''' }} : {{Partie{{{type|}}}|[[Mathc complexes/069| L'équation d'un '''polynôme''']]}} : : . : {{Partie{{{type|}}}| '''Partial Pivoting''' }} : {{Partie{{{type|}}}|[[Mathc_complexes/a35| Variables libres]]}} : {{Partie{{{type|}}}|[[Mathc_complexes/a157| Rendre un système '''compatibles''' en introduisant des paramètres]]}} : . : {{Partie{{{type|}}}| Application : '''Partial Pivoting''' }} : {{Partie{{{type|}}}|[[Mathc complexes/05h| Équilibrer une équation '''chimique''' dans '''R''']]}} : . : {{Partie{{{type|}}}| '''Les bases''' }} : {{Partie{{{type|}}}|[[Mathc_complexes/a220| Trouver une base pour ... ]]}} : {{Partie{{{type|}}}|[[Mathc_complexes/c233| Matrices de changement de base]]}} : {{Partie{{{type|}}}|[[Mathc complexes/03p| Matrice d'une application linéaire par rapport à la base "B"]]}} : . : {{Partie{{{type|}}}| '''Les projections''' }} : {{Partie{{{type|}}}|[[Mathc complexes/a245| Projection sur un sous-espace vectoriel]]}} : {{AutoCat}} ica2xsxn1gjdpuwl9r8vxzwflug97hz Les moteurs de rendu des FPS en 2.5 D/L'historique des moteurs de FPS 0 80643 746074 740277 2025-07-06T01:04:10Z Mewtow 31375 /* id Software : Wolfenstein, DOOM, Quake */ 746074 wikitext text/x-wiki Dans les années 2000, le FPS a subit de nombreuses transformations. Des Fast-FPS d'antan, nerveux et aux maps non-linéaires et "labyrinthiques", ont été progressivement passé à des jeux plus lents, plus scénarisés, plus scriptés, aux maps plus linéaires. Sans doute que l'arrivée de la 3D a entrainé beaucoup de transformations dans le domaine du JV, le FPS a lui aussi été marqué par l'évolution technologique. Mais les FPS datant d'avant l'arrivée de la 3D ont marqué leur époque pour leur gameplay très nerveux, très bourrin, avec une grande variété d'armes et de déplacements, et des maps complexes et non-linéaires, qu'on ne retrouve plus dans les jeux vidéos d'aujourd'hui. C'en est au point où les FPS d'antan sont actuellement appelés des Boomer-shooters, terme quelque peu cavalier auquel nous préférerons le terme de Fast-FPS, en opposition aux Slow-FPS d'aujourd'hui. Les fast-FPS, aussi appelés Boomer-FPS, regroupent de nombreuses sagas : les DOOM (sauf le 3), les Quake, les Unreal (sauf le 2), Duke Nukem en FPS, les Serious Sam, Heretic/Hexen, Marathon, Tribes, etc. [[File:FPSChart.svg|centre|vignette|upright=3|FPS les plus connus : historique]] L'histoire du FPS est intimement lié à celle des moteurs graphiques. L'invention des premiers FPS va de pair avec la création des premiers moteurs capables de rendre respectivement de la 2.5D et ensuite de la 3D. Après tout, difficile de faire de la vue subjective si l'on ne sait pas effectuer un rendu en 3D en temps réel. La technologie a donc joué un rôle déterminant au début du FPS. Et nous allons étudier les moteurs de ces vieux jeux en 2.5D. ==Un aperçu des moteurs de jeux de l'époque== Les premiers FPS utilisaient des moteurs qui ne sont totalement en 3D, mais sont plus des jeux en 2,5 D, à savoir qu'ils mélangent des éléments purement 2D, avec un rendu 2D simule un rendu 3D. L'illusion de la 3D est rendu par des techniques assez diverses, dont nous parlerons dans les prochains chapitres. Pour résumer, il y a en gros deux techniques principales : le ''raycasting'' et le ''portal rendering''. Les différents moteurs utilisent soit l'une soit l'autre, mais il arrive qu'ils utilisent les deux. La grande famille des moteurs de rendu 2.5D des FPS est composée de plusieurs lignées. La première est celle des jeux IdSoftware, avec les moteurs des jeux Wolfenstein 2D et DOOM. Et contrairement à ce qu'on pourrait croire, les deux utilisent des moteurs très différents. Le premier moteur d'IdSoftware est celui de Wolfenstein 3D, mais aussi de ses prédécesseurs de la saga Catacomb (oui, Wolfenstein 3D n'est pas le premier FPS inventé, nous en reparlerons). Il n'a pas de nom, contrairement à son successeur, le moteur des jeux DOOM, appelé l''''IdTech 1'''. Les moteurs de jeux suivants seront l''''Idtech 2, 3, 4''', respectivement utilisées pour Quake 1/2, puis Quake 3, et enfin DOOM 3. Bien qu'ils portent des noms similaires, ce sont des moteurs indépendants, bâtis sur des fondations totalement différentes. Le code source de tout ces moteurs a été rendu public, on sait comment ils fonctionnent. Moins connu, le '''moteur Build''' a été utilisé pour les jeux de 3D Realms : Duke Nukem 3D, Blood, Shadow Warrior, mais aussi les jeux de la saga WitchHaven. Quelques jeux indépendants récents sont développés sur ce moteur, notamment le jeux Ion Fury développé par les anciens programmeurs de Duke Nukem 3D. Le code source a été rendu public, ce qui fait qu'on sait comment fonctionne le moteur. Il utilise la technique du ''portal rendering'', dans sa forme la plus simple. Il n'implémente pas d'optimisations comme l'usage d'un BSP, comme le fait un DOOM pourtant sorti avant lui. Il est donc moins optimisé que DOOM, mais cela n'a pas posé problème car les jeux Build sont sortis quelques années après DOOM, dans une période où la technologie évoluait très vite et où les processeurs devaient très rapidement plus puissants. Moins connu, il faut aussi citer les moteurs d'autres jeux plus confidentiels de l'époque. Et commençons par les trois jeux de la saga Marathon, développés par Bungie (les développeurs de Halo), qui disposaient de leur propre moteur de jeux. Les trois jeux sont sortis sur Macintosh, et n'étaient pas compatibles avec Windows. De là, on peut rapidement deviner qu'ils utilisaient un moteur de jeu fait maison, sur lequel on ne sait malheureusement pas grand chose. Le moteur était cependant assez puissant et avec beaucoup de fonctionnalités : support des escaliers, d'une gestion de la hauteur, plafonds et sols de hauteur variable, des ascenseurs, etc. Il était aussi possible d'avoir plusieurs pièces superposées. La société Lucas Art, connue pour ses jeux Point'click, a développé deux FPS de son temps. Le premier est le jeu Star Wars : Dark Forces dans lequel on incarne un soldat de la résistance dans l'univers de Star Wars, le second est le FPS Outlaws se passant dans un univers de Western. Les deux jeux utilisaient un moteur de jeu fait maison, appelé le '''Jedi engine'''. Ce furent les deux seuls jeux à utiliser ce moteur, Lucas Arts ayant abandonné les FPS par la suite. LE fait que Outlaws soit sorti alors que Quake était déjà sorti n'a pas aidé ses ventes, la 3D venait d'arriver, il n'y avait plus besoin d'un rendu en 2.5D. Le moteur a été abandonné après cela. Le code source du moteur n'est pas disponible et n'a jamais été rendu public, mais quelques fans de ces jeux ont effectué de la rétro-ingénierie pour retrouver un moteur équivalent. Le moteur de jeu utilise la technique du ''portal rendering'', la même que le moteur Build. Les jeux de la saga Ultima Underwolrd étaient des jeux utilisant un moteur en quasi-3D. Il s'agit d'une série de FPS-RPG sortis un tout petit peu après Wolfenstein 3D. Des tentatives de ''reverse enginnering'' du moteur sont encore en cours. La seule chose que l'on sait est que les niveaux du jeu sont stockés en 2D, avec une organisation basée sur des ''tiles'', mais que cette représentation était utilisée comme base pour un rendu en pure 3D. D'après les dires de Doug Church, un des programmeurs du jeu, voici comment fonctionnait ce moteur : : {{g|However, let me second what Dan Schmidt said in the guestbook back in August about the description of the UW engine you guys have up on the page. Namely, UW _was not_ a raycasting engine. While UW did use a tilemap to store the world, that has nothing to do with the rendering model. In general, I'd suggest that the "world rep" and "rendering engine" be considered separate things when discussing game technology, because they very often are. In UW, we had a tile based world. The renderer used the tiles to explore the view cone and do a high level cull operation. From the list of visible tiles, we generated full 3d polygons, in the traditional XYZ sense, and handed them off to a rendering back end and rendered each poly in 3d as is. That is also how the 3d models like the ankh shrines or benches were done, which clearly aren't "raycast" model 3d objects. Now, in practice, many of our 3d primitives did things like normal checks first, and then chose which algorithim to rasterize with based on scale/normal/etc of the surface being rendered.}} Source : [https://www.peroxide.dk/underworld.shtml ultima Underworld Viewer ] Enfin, il faut aussi citer quelques FPS sortis sur Amiga, qui utilisaient leur propre moteur de rendu. Des jeux comme Alien Breed 3D et quelques autres étaient dans ce genre. Le code machine de ces jeux est disponible et a été rendu public, mais peu d'informations sont connues à ce jour sur le fonctionnement de leur moteur. Paradoxalement, il a existé des moteurs de jeux en 3D avant même que les premiers moteurs de jeu en 2.5D n'apparaissent. Il faut notamment noter le '''''Freescape 3D engine''''', datant de quelques années avant le premier moteur d'IdSoftware ! C'était un moteur de jeu entièrement en 3D. [[File:Fpsengine.svg|centre|vignette|upright=3|Liste des moteurs de jeu en 2.5D et en 3D utilisés par les FPS au cours du temps.]] Pour résumer, beaucoup de FPS en 2.5D ont existé et chaque entreprise utilisait plus ou moins son propre moteur maison. Le moteur de Wolfenstein 3D a été utilisé pour quelques productions, le moteur de DOOM aussi, le moteur Build n'a été utilisé que pour trois jeux, les autres moteurs n'ont été utilisés que par leur entreprise créatrice. ==id Software : Wolfenstein, DOOM, Quake== id Software est une entreprise de jeux vidéo crée en 1991, par ses quatre membres fondateurs : John Carmack, John Romero, Tom Hall, Adrian Carmack (pas de liens familiaux avec John Carmack). Les deux premiers membres, les plus connus, ont décidé de quitter l'entreprise SoftDisk dans laquelle ils codaient des jeux vidéo, pour fonder leur propre studio de développement. Ils recrutèrent alors les deux autres membres. Le récit de la vie de cette entreprise, de la création jusqu'à environ 1996, est raconté dans le livre "Masters of Doom: How Two Guys Created an Empire and Transformed Pop Culture", publié en 2004, qui est la référence pour tout fan des jeux de cette entreprise. John Carmack est le créateur des moteurs graphiques utilisés dans Wolfenstein 3D, DOOM, Quake et bien d'autres. Il est le programmeur principal, même si les autres membres étaient doués en programmation (Romero a appris à coder en autodidacte et a participé activement au développement de tous les jeux id Software. Il est reconnu pour être capable d'implémenter des idées autrefois publiées dans la littérature académique en rendu graphique, d'une manière qui fait que ces algorithmes peuvent tourner en temps réel. Romero est le level-designer principal des jeux, aux côtés de Tom Hall, Sandy Petterson, et d'autres membres qui ont participé à la création des jeux d'id Software. Le tout premier jeu en vue subjective temps-réel d'id Software était Hovertank, un jeu de Tank en vue subjective qu'on trouve facilement en abandonware. Il a été le premier jeu à utiliser ce genre de rendu, et le moteur était très simpliste. Le gameplay est franchement pauvre et le jeu est clairement une démo technologique, qui permet de montrer ce que peut faire un moteur simple. Il n'y a même pas de gestion des textures, chaque mur, sol et plafond a une couleur unie, sans détails. Wolfenstein 3D n'était pas le premier FPS, contrairement à ce qu'on pourrait croire. Catacomb 3D eu trois suites, The Catacomb: Abyss est sorti en même temps que Wolfenstein 3D, The Catacomb: Armageddon est sorti la même année, The Catacomb: Apocalypse est sorti en 1993, après W3D. Peu de choses sont connues sur ce moteur, mais quelques informations ont fuité dans une interview de Carmack par Fabien Sanglard, disponible via ce lien [https://fabiensanglard.net/doom3/interviews.php Doom3 Source Code Review: Interviews (Part 6 of 6)]. Apparemment, le moteur de Catacomb 3D utilisait une technique différente de celle utilisée dans le jeun suivant d'Id Software : Wolfenstein 3D. : ''The internal rendering was very different from Catacombs 3D. Catacombs used basically a line- rasterization approach, whii Wolfenstein used a much more robust ray-casting approach. But the end result was that they rendered the same pictures.'' Wolfenstein 3D, est souvent pris à tord comme le premier FPS, car il a marqué les esprits, bien plus que les Catacomb, qui sont restés très confidentiels. Après la sortie de Wolfenstein 3D, d'autres entreprises ont utilisé ce moteur, avec l'aval d'id Software et moyennement rémunération. En somme, dès le début du FPS, on avait ce système où un moteur de jeu créé par une entreprise était vendu à d'autres pour que ces dernières créent leurs propres jeux vidéos avec. Le moteur de Wolfenstein 3D a été réutilisé dans de nombreuses productions, dont voici la liste : * Blake Stone: Aliens of Gold * Blake Stone: Planet Strike * Corridor 7: Alien Invasion * Cybertag * Hellraiser * Operation Body Count * Rise of the Triad * Super 3D Noah's Ark Beaucoup de ces jeux tombèrent dans l'oubli, parce qu'ils étaient de véritables catastrophes vidéoludiques à la qualité plus que mauvaise. De plus, l'arrivée de DOOM l'année suivante fit que le moteur de Wolfenstein 3D était devenu trop limité et obsolète (après seulement un an...). Seuls baroud d'honneur, les jeux "ShadowCaster" et "In Pursuit of Greed", de Raven Software, utilisaient une version améliorée du moteur de Wolfenstein 3D. Le moteur ajoutait un éclairage limité, des murs de taille variable, des sols pentus et des sols/plafonds texturés. Wolfenstein 3D a ensuite été suivi par DOOM, puis par DOOM 2, deux jeux d'exception, dont le moteur était complétement différent de celui de Wolfenstein 3D. Le moteur de W3D utilisait une technique particulière de ''raycasting'', mais pas le moteur de DOOM. Le développement de l'IdTech 1, le moteur de DOOM 1 et 2, a commencé peu après la sortie de Wolfenstein 3D et son histoire est assez intéressante. John Carmack pensait à la base créer un moteur basé sur la technique dite du ''portal rendering'', comme le fera Duke Nukem 3D quelques années plus tard. Mais John Romero, mappeur de l'équipe joua un mauvais tour à Carmack. Une des maps qu'il avait conçu ramait tellement que Carmack du retourner au travail et trouver une optimisation pour résoudre le problème. En parallèle de son travail sur le moteur de DOOM, Carmack travaillait en parallèle sur le portage de Wolfenstein 3D sur Super Nintendo. Mais la machine n'était même pas assez puissante pour faire tourner le jeu. Aussi, Carmack fi quelques recherches pour trouver une solution. Il découvrit dans plusieurs papiers de recherche la technique dite du BSP (''Binary Space Partitioning''), et décida de réécrire complétement le moteur de Wolfenstein 3D avec cette technique. La technique du ''raycasting'' était passée à la trappe au profit d'un système de ''portal rendering'' amélioré. Et c'est cette technique qui a été utilisée pour DOOM ! ==Les source ports== [[File:Doom-ports.svg|thumb|Doom source ports]] <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | next=Les généralités : le rendu 2D | nextText=Les généralités : le rendu 2D }} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 4l4n5qopwx6tcag2g1tcejux4si2r9q Les moteurs de rendu des FPS en 2.5 D/Les généralités : le rendu 2D 0 81107 745965 723786 2025-07-05T12:16:47Z Mewtow 31375 745965 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culmling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] ahcag6zqx1eyiqchxz3cy6ic7714g08 745966 745965 2025-07-05T12:31:00Z Mewtow 31375 745966 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] aq5aeap7oer1ps8r6ypr5z9o5xr4wic 745967 745966 2025-07-05T12:33:05Z Mewtow 31375 /* L'algorithme du peintre */ 745967 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 5l1trbj4d9a29fl02cwmr660pi8fv8u 745968 745967 2025-07-05T12:33:12Z Mewtow 31375 /* L'algorithme du peintre */ 745968 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] ryuy9flbhjcbi03lk4ejed6bh2mdymj 745969 745968 2025-07-05T12:33:43Z Mewtow 31375 /* L'algorithme du peintre */ 745969 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 5a2rl3bj08bt6kjuwd39f45iq5gpiz8 745970 745969 2025-07-05T12:34:16Z Mewtow 31375 /* Le rendu d'un jeu à la DOOM */ 745970 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] nb7pts51mvx45fx36qmsiqzv4rajckw 745971 745970 2025-07-05T12:36:30Z Mewtow 31375 /* Le rendu en 3D, partiel ou total */ 745971 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. Ils trient implicitement les objets selon leur distance, pour les rendre dans l'ordre adéquat. Le tri en question est rarement réalisé avec une opération de tri. Les niveaux étaient représentés en mémoire d'une manière qui permette de déterminer dans quel ordre rendre les objets facilement. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] pddf6uambgwceorwjkh2zzc3sjfsqt7 745972 745971 2025-07-05T12:37:42Z Mewtow 31375 /* Les grandes méthodes de détermination des surfaces visibles */ 745972 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 2athvayxvmsfmz5c6tcrfaz9zdy2roj 745973 745972 2025-07-05T12:38:02Z Mewtow 31375 /* L'algorithme du peintre */ 745973 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 2fl5rt728lls5eka0xidfpco3d61jx9 745974 745973 2025-07-05T12:38:27Z Mewtow 31375 /* Les grandes méthodes de détermination des surfaces visibles */ 745974 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites'' qui sera vu dans la section suivante. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 6eeajm5cku6r6px6jvezrdq19513lp9 746029 745974 2025-07-05T18:08:37Z Mewtow 31375 /* Le rendu de l'arme */ 746029 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] oyqaima7103lzpojzupijwg8wyvwh6s 746030 746029 2025-07-05T18:08:53Z Mewtow 31375 /* Le rendu des ennemis et items */ 746030 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. ==Le rendu en 3D, partiel ou total== Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ===La détermination des surfaces visibles=== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 299x3ra403mw40uiojtmpb6dar2rvet 746062 746030 2025-07-06T00:21:14Z Mewtow 31375 746062 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] expn4j39j615dt0yiyd5htg6qd346l7 746063 746062 2025-07-06T00:34:31Z Mewtow 31375 /* L'algorithme du peintre */ 746063 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 3m7su056rqklaw2dalk9y7h3ld804as 746064 746063 2025-07-06T00:34:45Z Mewtow 31375 /* L'algorithme du peintre */ 746064 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 93ef926hsvqk2d6arwwtj2cmudu28bw 746065 746064 2025-07-06T00:42:50Z Mewtow 31375 /* Les défauts des rendus proche-vers-loin et loin-vers-proche */ 746065 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] fhh0ibkp12q3ibro0yr7mn55tv4jek5 746066 746065 2025-07-06T00:42:59Z Mewtow 31375 /* Les défauts des rendus proche-vers-loin et loin-vers-proche */ 746066 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] mhying3fd220mjiliqwy8rtka2p6fdb 746067 746066 2025-07-06T00:44:58Z Mewtow 31375 /* Les défauts des rendus proche-vers-loin et loin-vers-proche */ 746067 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] aq9wuqzp388pac7xeysj5l5xktusiej 746068 746067 2025-07-06T00:45:41Z Mewtow 31375 /* L'algorithme du peintre : le rendu loin-vers-proche */ 746068 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 4sarllq2l98j9vrfh7d96num3uhfiin 746069 746068 2025-07-06T00:45:52Z Mewtow 31375 /* L'algorithme du peintre : le rendu loin-vers-proche */ 746069 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] pkmh5e46o0hjw3egamwig5xvea7rhz0 746070 746069 2025-07-06T00:47:06Z Mewtow 31375 /* L'algorithme du peintre : le rendu loin-vers-proche */ 746070 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres, pour obtenir l'image finale. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. L'image final est rendue dans une portion de mémoire RAM appelée le ''framebuffer'', superposer une image dessus revient à la copier dans cette zone de mémoire. La copie peut être partielle ou totale, tout dépend des besoins. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] axoqbd7k1ni3tqzq58g67yzpittuztq 746071 746070 2025-07-06T00:49:35Z Mewtow 31375 /* Les défauts des rendus proche-vers-loin et loin-vers-proche */ 746071 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres, pour obtenir l'image finale. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. L'image final est rendue dans une portion de mémoire RAM appelée le ''framebuffer'', superposer une image dessus revient à la copier dans cette zone de mémoire. La copie peut être partielle ou totale, tout dépend des besoins. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. Dans un FPS, il y a une classe d'objets qui ne peuvent pas être rendus avec la technique du BSP ou du portal rendering : les ennemis. Ils bougent d'une manière qui n'est pas prévue par un script, mais par un système d'IA, aussi rudimentaire soit-il. Impossible de précalculer le mouvement des ennemis ou autres. Autant les murs peuvent être rendus avec un BSP ou des portals, autant il faut trouver une autre solution pour les ennemis. La solution retenue est de rendre les ennemis à part du reste. DOOm faisait ainsi : il rendait les murs, puis les ennemis et autres objets basiques, en utilsant des ''sprites''. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] 47rs6uq2xbvcur48vqs7ukb85aedsra 746072 746071 2025-07-06T00:54:00Z Mewtow 31375 /* Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM */ 746072 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres, pour obtenir l'image finale. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. L'image final est rendue dans une portion de mémoire RAM appelée le ''framebuffer'', superposer une image dessus revient à la copier dans cette zone de mémoire. La copie peut être partielle ou totale, tout dépend des besoins. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. L'avantage est que cet algorithme fait moins d'écriture. Avec l'algorithme du peintre, il arrive qu'on dessine des objets qui sont ensuite marqués totalement par un objet plus proche. Ou encore, on dessine un objet en entier, mais une partie de celui-ci est ensuite masquée. On dessine donc inutilement. Et ces dessins correspondent à écrire des pixels dans le ''framebuffer'', donc à de la puissance de calcul, des transferts mémoire inutiles. Des pixels sont écrits pour ensuite être écrasés. C'est le problème de l'''''overdraw''''', que nous traduiront en français par le volontairement ridicule terme de '''sur-dessinage'''. Avec le rendu proche-vers-loin, chaque pixel est écrit une seule et unique fois, le sur-dessinnage disparait ! Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. Dans un FPS, il y a une classe d'objets qui ne peuvent pas être rendus avec la technique du BSP ou du portal rendering : les ennemis. Ils bougent d'une manière qui n'est pas prévue par un script, mais par un système d'IA, aussi rudimentaire soit-il. Impossible de précalculer le mouvement des ennemis ou autres. Autant les murs peuvent être rendus avec un BSP ou des portals, autant il faut trouver une autre solution pour les ennemis. La solution retenue est de rendre les ennemis à part du reste. DOOm faisait ainsi : il rendait les murs, puis les ennemis et autres objets basiques, en utilsant des ''sprites''. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] pfmw3ssdg132m8zry5xrqyrtkoxqein 746073 746072 2025-07-06T00:54:11Z Mewtow 31375 /* Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM */ 746073 wikitext text/x-wiki [[File:RaycastingGameMaker.jpg|vignette|Raycasting sur GameMaker.]] Les tout premiers First Person Shooters, comme DOOM ou Wolfenstein 3D, avaient un rendu relativement simpliste. Et contrairement à ce que voient nos yeux, le rendu n'était pas toujours en 3D. Ce n'était pas de la vraie 3D, mais une méthode de rendu hybride entre 2D et 3D. Quelques moteurs utilisaient un moteur en 3D, avec rendu d'objets en polygones, comme le moteur d'Ultima Underworld, mais ils sont plus rares. En tout cas, les ordinateurs de l'époque avaient assez de puissance pour rendre des graphismes en 3D, avec un résultat certes rudimentaire, mais fonctionnel. Néanmoins, le rendu des jeux vidéos actuels est fortement différent de ce qui se faisait à l'époque. Mais pour comprendre pourquoi, il faut faire une petite introduction sur les méthodes de rendu 3D en général. Calculer un monde en 3D ou en 2.5D, demande de faire beaucoup de choses différentes : calculer la géométrie des niveaux, appliquer les textures sur les modèles 3D, calculer les lumières, les ombres, etc. La distinction géométrie, textures, éclairage est assez intuitive. Et les moteurs de jeu savent relativement bien gérer ces trois aspects. Les algorithmes d'éclairage se sont améliorés avec le temps, le placage de texture est un problème quasiment résolu, le calcul de la géométrie a lentement évolués avec l'évolution des API 3D, etc. Mais ce n'est pas le seul. ==La détermination des surfaces visibles== Par contre, un autre problème très important pour le rendu graphique est celui de la '''visibilité''' des objets à l'écran, à savoir déterminer qu'est ce qui est visible à l'écran. Le moteur graphique doit déterminer quels sont les objets visibles à l'écran et ceux qui ne le sont pas. Et cela regroupe trois choses différents : * Un objet en-dehors du champ de vision doit être éliminé du rendu : c'est ce qu'on appelle le ''frustrum cliping''. * Un objet 3D a une face visible et une face cachée qui fait dos à la caméra : la face cachée n'est pas rendue grâce à des techniques de ''back-face culling''. * Si un objet est masqué par un autre, totalement ou partiellement, il ne faut pas rendre ce qui est masqué. : on parle d'''occlusion culling''. Et ce qui est intéressant, c'est que la détermination de la visibilité est un problème central, qui détermine comment fonctionne le moteur d'un jeu. Les autres problèmes sont en quelque sorte secondaire, la manière dont un moteur de jeu fonctionne dans les grandes lignes est gouvernée par ce problème de visibilité. A ce propos, il est intéressant de regarder ce qu'en dit Michael Abrash, un des programmeurs ayant codé les moteurs de Quake et d'autres jeux Id Software aux côtés de John Carmack, dont la postérité n'a pas retenu son nom. Voici une citation tirée de son livre "Graphics Programming Black Book Special Edition", où il parle de son expérience sur le moteur de Quake: : ''In terms of graphics, Quake is to DOOM as DOOM was to its predecessor, Wolfenstein 3-D. Quake adds true, arbitrary 3-D (you can look up and down, lean, and even fall on your side), detailed lighting and shadows, and 3-D monsters and players in place of DOOM’s sprites. Someday I hope to talk about how all that works, but for the here and now I want to talk about what is, in my opinion, the toughest 3-D problem of all: visible surface determination (drawing the proper surface at each pixel), and its close relative, culling (discarding non-visible polygons as quickly as possible, a way of accelerating visible surface determination). In the interests of brevity, I’ll use the abbreviation VSD to mean both visible surface determination and culling from now on.'' : ''Why do I think VSD is the toughest 3-D challenge? Although rasterization issues such as texture mapping are fascinating and important, they are tasks of relatively finite scope, and are being moved into hardware as 3-D accelerators appear; also, they only scale with increases in screen resolution, which are relatively modest.'' : ''In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.'' ===Les grandes méthodes de détermination des surfaces visibles=== Les solutions à ce problème de visibilité sont assez nombreuses. Heureusement, elles peuvent se classer en quelque grand types assez larges. Les techniques les plus utilisées sont le tampon de profondeur (''z-buffer''), le tri de polygones par profondeur (''Depth sorting''), le lancer de rayon (''raytracing''). Elles résolvent le problème de la visibilité d'une manière fort différente. Les jeux 3D modernes utilisent un tampon de profondeur, car c'est une technique supportée par les cartes graphiques modernes. Elles peuvent donc l'utiliser avec de bonnes performances, vu que le GPU fait la grosse partie du travail. Mais les premières cartes graphiques ne géraient pas de tampon de profondeur, ce n'est que vers la fin des années 90 que la technique a été intégrée aux GPU. Et une implémentation logicielle du tampon de profondeur aurait été beaucoup trop lente. A vrai dire, les GPU de l'époque ne géraient pas grand chose. Les cartes accélératrices 3D n'existaient pas encore et les GPU ne faisaient que de la 2D. Et cette 2D était souvent très rudimentaire. Les cartes graphiques des PC de l'époque étaient optimisées pour rendre du texte, avec un support minimal du rendu d'images par pixel. En conséquence, les jeux vidéos de l'époque devaient faire tous les calculs graphiques sur le CPU, on parlait alors de rendu logiciel, de rendu ''software''. Et les contraintes faisaient qu'ils devaient utiliser des algorithmes particuliers pour résoudre la visibilité. Le moteur de Wolfenstein 3D a été le seul à utiliser la technique du lancer de rayons. Et encore, il ne l'utilisait que d'une manière bien particulière, avec beaucoup d'optimisations qui rendaient son calcul bien plus simple : les niveaux du jeu étaient en 2D, le rendu était fait colonne par colonne et non pixel par pixel, les niveaux étaient alignés sur une grille 2D, et j'en passe. Mais la technique était très gourmande en puissance de calcul. Mais le vrai problème est que les optimisations appliquées bridaient les concepteurs de niveaux. Impossible de faire autre chose qu'un labyrinthe aux murs à 90°... Tous les autres jeux vidéos, que ce soit DOOM ou le Build engine, ou tous les autres moteurs de l'époque, utilisaient des méthodes dites de '''rendu ordonné''', qui consistent à rendre les objets à rendre du plus proche au plus lointain ou inversement. La méthode la plus simple pour cela est celle de l’algorithme du peintre. Et les premiers FPS utilisaient une version fortement améliorée de cet algorithme. ===L'algorithme du peintre : le rendu loin-vers-proche=== La base d'un rendu en 2D ou 2.5D est de superposer des images 2D pré-calculées les unes au-dessus des autres, pour obtenir l'image finale. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, une image pour chaque mur, etc. L'image final est rendue dans une portion de mémoire RAM appelée le ''framebuffer'', superposer une image dessus revient à la copier dans cette zone de mémoire. La copie peut être partielle ou totale, tout dépend des besoins. Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les objets à rendre du plus lointain au plus proche. L'idée est que si deux objets se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Il s'agit de l''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. {| |[[File:Polygons cross.svg|vignette|Polygons cross]] |[[File:Painters problem.svg|vignette|Painters problem]] |} ===Le rendu proche-vers-lointain des jeux en 2.5D à la DOOM=== Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. L'idée était de rendre l'image dans le sens inverse, du plus proche au plus lointain. La technique mémorisait quelles portions de l'image ont déjà été écrites, afin qu'elles ne soient pas modifiées ultérieurement. Ainsi, si on veut rendre un objet lointain partiellement caché par un objet proche, la portion non-cachée de l'objet correspondra à une portion vierge de l'image, mais la portion cachée correspondra à une portion déjà écrite. La portion non-cachée écrira ses pixels dans le ''framebuffer'', pas la portion cachée. L'occlusion est donc gérée convenablement. L'avantage est que cet algorithme fait moins d'écriture. Avec l'algorithme du peintre, il arrive qu'on dessine des objets qui sont ensuite marqués totalement par un objet plus proche. Ou encore, on dessine un objet en entier, mais une partie de celui-ci est ensuite masquée. On dessine donc inutilement. Et ces dessins correspondent à écrire des pixels dans le ''framebuffer'', donc à de la puissance de calcul, des transferts mémoire inutiles. Des pixels sont écrits pour ensuite être écrasés. C'est le problème de l''''''overdraw''''', que nous traduiront en français par le volontairement ridicule terme de '''sur-dessinage'''. Avec le rendu proche-vers-loin, chaque pixel est écrit une seule et unique fois, le sur-dessinnage disparait ! Il n'y a pas de nom pour ce type de rendu, à ma connaissance, mais je vais l'appeler le '''rendu proche-vers-loin''', opposé au '''rendu loin-vers-proche''' de l’algorithme du peintre. Cependant, le rendu proche-vers-loin ne marche qu'à une seule condition : que les objets rendus ne soient pas transparents. Dès que de la transparence est impliquée, le rendu proche-vers-loin ne marche plus trop. DOOM gérait la situation assez simplement en mélangeant algorithme du peintre et rendu proche-vers-loin. les murs étaient rendus en proche-vers-loin, alors que les ''sprites'' et les objets transparents étaient rendus en loin-vers-proche. ===Les défauts des rendus proche-vers-loin et loin-vers-proche=== Un problème est que les rendus proche-vers-loin et du peintre demandent de trier les polygones d'une scène selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Un problème est que trier les polygones demande d'utiliser un algorithme de tri, dont le nombre d'opérations est assez important. Le temps de calcul est proportionnel à <math>N \times log{N}</math>, d'après la théorie de la complexité algorithmique. Heureusement, quelques optimisations permettent de réduire ce nombre d'opération d'une manière assez drastique et de passer à un temps de calcul simplement proportionnel à N, linéaire. De plus, ces optimisations permettent de faire ce tri très facilement, sans avoir à tout retrier quand le joueur tourne la caméra ou se déplace. Une optimisation de ce type est l'usage du '''''Binary Space Partionning'''''. Concrétement, elle est utilisée pour précalculer des informations spatiales, qui permettent de trier les objets du plus lointain au plus proche. le BSP est formellement un arbre binaire dont le parcours permet de trier naturellement les objets/murs soit du plus proche au plus loin, soit, au contraire du plus loin au plus proche. Tout dépend de comment on le parcours, il y a deux méthodes différentes, une par sens de transfert. Elle a été utilisée dans le moteur de DOOM 1 et 2, qui étaient formellement en 2.5D, mais aussi dans des jeux 3D d'Id Software comme Quake 1, Quake 2, et même DOOM 3. Les autres jeux en 2.5D de l'époque de DOOM faisaient autrement. Ils utilisaient la technique du '''''portal rendering'''''. Elle aussi précalculait des informations spatiales qui permettaient de trier naturellement les objets du plus lointain au plus proche. Pour simplifier, la map était coupées en pièces, connectées les unes aux autres par des ''portals''. Il y avait un ''portal'' pour chaque porte, fenêtre, ouverture dans une pièce. Le moteur commençait le rendu dans la pièce actuellement occupée et rendait les objets. Puis, il déterminait les ''portal''visibles depuis la caméra, les portals visibles apr le joueur. Pour chaque ''portal visible'', il rendait alors la pièce voisine visible depuis ce ''portal'' et continuait récursivement le rendu dans les pièces voisines. Le défaut des portal et des BSP est qu'ils marchent bien quand la géométrie du niveau est statique, qu'elle n'est pas modifiée. Par contre, si le niveau doit subir des modifications dynamiques, c'est plus compliqué. Avec un BSP, les niveaux dynamiques sont compliqués à concevoir, car modifier un BSP dynamiquement n'est pas chose facile. A la rigueur, si les modifications peuvent être scriptées, les choses sont plus faciles. Avec des portals, modifier un niveau est plus simple, plus facile, mais loin d'être facile hors-scripts. Dans un FPS, il y a une classe d'objets qui ne peuvent pas être rendus avec la technique du BSP ou du portal rendering : les ennemis. Ils bougent d'une manière qui n'est pas prévue par un script, mais par un système d'IA, aussi rudimentaire soit-il. Impossible de précalculer le mouvement des ennemis ou autres. Autant les murs peuvent être rendus avec un BSP ou des portals, autant il faut trouver une autre solution pour les ennemis. La solution retenue est de rendre les ennemis à part du reste. DOOm faisait ainsi : il rendait les murs, puis les ennemis et autres objets basiques, en utilsant des ''sprites''. ==Le rendu d'un jeu à la DOOM== Le rendu des murs est de loin la portion la plus compliquée du moteur. Aussi, nous allons d'abord parler du reste dans ce chapitre. Cependant, nous verrons que le rendu s'effectue ne plusieurs étapes, qui se font dans un ordre différent de celui du chapitre. L'ordre de ce rendu s'explique assez facilement quand on connait comme est effectué le rendu d'un jeu en 2 pure. Et je précise bien en 2D pure, car le principe de la 2.5D est assez similaire. La base d'un rendu en pure 2D, comme un jeu Mario, superpose des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des '''sprites'''. Le rendu des ''sprites'' doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les ''sprites'' qui se superposent sur les autres) : on parle d''''algorithme du peintre'''. [[File:Painter's algorithm.svg|centre|vignette|upright=2.0|Exemple de rendu 2D utilisant l'algorithme du peintre.]] Dans un FPS en 2.5D, l'idée est de calculer séparément une image pour l'environnement, le décor, une image avec les ennemis, une image avec l'arme en avant-plan et une autre image sur le HUD. Le HUD est à l'avant-plan et prédomine tout le reste, ce qui fait qu'il doit être dessiné en dernier. L'arme est située juste après le HUD, elle est tenue par le personnage et se situe donc devant tout le reste : elle est dessinée en second. Pour l'environnement et les ennemis, tout dépend de la pièce dans laquelle on est et de la position des ennemis. Mais il est plus pratique de rendre le décor et de dessiner les ennemis dessus, l'environnement servant d'arrière-plan. Le rendu s'effectue donc comme ceci : * d'abord le rendu des murs, du plafond et du sol ; * puis le rendu des ennemis et items ; * puis le rendu de l'arme ; * et enfin le rendu du HUD. Le moteur de ces jeux disposait de deux méthodes de rendu assez simples : * un rendu purement 2D, pour le HUD, l'arme, les ennemis et les items ; * un rendu en 2.5D ou en 3D pour les murs. ===Le rendu du HUD=== [[File:Cube screenshot 199627.jpg|vignette|Cube screenshot 199627]] Le HUD est simplement rendu en 2D, car le HUD est fondamentalement une structure en 2D. Il est mis à jour à chaque fois que le joueur tire (compteur de munitions), perd de la vie, prend de l'armure, etc. Il est idéalement rendu à la toute fin du rendu, son sprite étant superposé à tout le reste. La raison à cela est que le HUD est plein, et non transparent. J'explique. Un HUD de FPS normal ressemble à ce qu'il y a dans le screenshot à droite : quelques chiffres et icônes superposées sur le reste du rendu. On voit l'icône pour la vie, sa valeur, idem pour l'armure et les munitions. Mais entre les icônes et les chiffres, on voit la scène, l'eau, la texture du sol, etc. Le HUD a donc des parties transparentes, dans le sens où seuls les chiffres et icônes sont visibles/opaques. [[File:Advanced raycasting demo 2.gif|vignette|Advanced raycasting demo 2]] Mais dans Wolfenstein 3D, et dans d'autres jeux du même genre, on a la même chose que ce qui est illustré dans l'animation de droite. Le HUD est un gros rectangle qui prend tout le bas de l'écran. On peut bouger autant qu'on veut, le bas de l'écran associé au HUD reste le même. Le HUD n'a pas de transparence, les textures et l'environnement se se voient pas à travers. Avec cette contrainte, on peut dessiner le HUD et le reste de l'image séparément. Le HUD est donc rendu au tout début du rendu. Cela signifie aussi que l'image calculée est en réalité plus petite. Si le HUD prend 10% de l'écran, alors on a juste à dessiner les 90% restants. Sans cette contrainte, on doit calculer 100% de l'image, pour ensuite superposer un HUD partiellement transparent. ===Le rendu des ennemis et items=== Les objets sont rendus avec de simples images placées au-dessus du décor, qui ne sont ni plus moins que des '''sprites'''. Le meilleur moyen pour s'en rendre compte étant de tourner autour d'un objet : la forme de l'objet ne change pas du tout. Les objets sont de simples pancartes sur lesquelles on plaque une image. [[File:Anarch short gameplay.gif|vignette|Anarch short gameplay]] Les ennemis sont généralement animés : ils bougent, ils sont "stun" quand on tire dessus, ils peuvent tirer des projectiles, ils ont une animation de mort, etc. Et il y a une animation pour chaque action. Pareil pour certains objets de l'environnement : pensez aux fameux barils explosifs qui explosent quand on tire dessus ! Le tout est illustré ci-contre, avec quelques exemples. Reste à la simuler avec des ''sprites''. Pour cela, rien de plus simple : chaque animation est toujours la même, elle correspond à une succession de ''sprites'' qui est toujours identique. Il suffit de dérouler la bonne succession de ''sprite'' et le tour est joué ! Le rendu des ''sprites'' se fait une fois que l'environnement a été dessiné, c'est à dire après les murs, le sol et les plafonds. Les ''sprites'' des ennemis et items sont donc superposé sur l'arrière-plan calculée par l'étape précédente. Cependant, certains sprites peuvent se recouvrir : il faut impérativement que le sprite le plus proche soit affiché au-dessus de l'autre. Pour cela, les sprites sont superposés suivant l'algorithme du peintre : on commence par intégrer les ''sprites'' des objets les plus lointains dans l'image, et on ajoute des ''sprites'' de plus en plus proches. Faire cela demande évidemment de trier les ''sprites'' à rendre en fonction de la profondeur des objets/ennemis dans le champ de vision (qu'il faut calculer). Le cas le plus simple est le ''sprite'' de l'arme, qui est dessiné en tout dernier, car il est le plus proche du joueur. Un autre point est qu'il ne suffit pas de superposer un ''sprites'' d'item ou d'ennemi pour que cela fonctionne. Cela marche dans un jeu en pure 2D, mais la 3D impose de gérer la profondeur. Rappelons que plus un objet/ennemi est loin, plus il nous parait petit, plus il est petit à l'écran. Les ''sprites'' doivent donc être mis à l'échelle suivant la distance : rapetissés pour les ennemis lointains, et zoomés pour les ennemis proches. Pour cela, on utilise une relation mathématique très simple : la loi de Thalès. Dans un jeu vidéo, et comme dans le monde réel, si on multiplie la distance d'un objet par deux, trois ou quatre, celui-ci devient respectivement deux, trois ou quatre fois plus petit. Dit autrement, un objet de hauteur H situé à une distance D aura une hauteur perçue identique à celle d'un objet de hauteur double/triple/quadruple situé deux/trois/quatre fois plus loin. En clair, pour un objet de hauteur <math>h_1</math>, situé à une distance <math>d_1</math>, et un autre objet de hauteur <math>h_2</math> et de distance <math>d_2</math>, les deux auront la même hauteur perçue : : <math>\frac{h_1}{d_1} = \frac{h_2}{d_2}</math> Tout ''sprite'' est une image, avec une résolution. Elle a un certain nombre de pixels à l'horizontale, un autre à la verticale. La taille verticale en pixel du ''sprite'' dépend du ''sprite'', mettons qu'elle est égale à 50 pixels de large pour 60 de haut. Il s'agit de la taille réelle du sprite déterminée lors de la conception du jeu (aussi bien en vertical et horizontal). Nous allons noter sa taille en vertical comme suit : <math>T_s</math>. Cette taille correspond à une distance précise. Pour un ''sprite'' 50 pixels de large pour 60 de haut, le ''sprite'' aura la même taille à l'écran à une certaine distance, que nous allons noter <math>D_s</math>. Maintenant, d'après la relation vue plus haut, on peut calculer la taille affichée à l'écran du ''sprite'', notée H. Pour cela, il suffit de connaitre la distance D, et on a la relation : : <math>\frac{T}{D} = \frac{T_s}{D_s}</math> On peut la reformuler comme suit : : <math>T = D \times \frac{T_s}{D_s}</math> Quelques multiplications, et le tour est joué. Le terme <math>\frac{T_s}{D_s}</math> peut même être mémorisé à l'avance pour chaque ''sprite'', ce qui économise quelques calculs. ===Le rendu de l'arme=== Le rendu de l'arme est assez particulier. Tous les jeux vidéos, même récents, utilisent un rendu séparé pour l'arme et le reste du monde. Pour les jeux en 2.5D, c'est parce que c'est plus simple de procéder ainsi. Pour les jeux en 3D, la raison est que cela évite de nombreux problèmes. Si l'arme était en 3D et était calculée comme le reste du rendu, on aurait des problèmes du genre : l'arme passe à travers les murs quand on se rapproche trop d'eux. Il y a bien des exceptions, mais elles sont assez rares. Le tout est bien expliqué dans cette vidéo Youtube sur la chaine de "Scrotum de Poulpe" (ca ne s'invente pas, je sais) : * [https://www.youtube.com/watch?v=cK866vpJtYQ Le "problème" des mains dans les FPS]. Sur les anciens jeux comme Wolfenstein 3D, DOOM, Duke Nukem et autres, l'arme utilise la technique des ''sprites''. <noinclude> {{NavChapitre | book=Les moteurs de rendu des FPS en 2.5 D | prev=L'historique des moteurs de FPS | prevText=L'historique des moteurs de FPS | next=Le moteur de Wolfenstein 3D | nextText=Le moteur de Wolfenstein 3D }}{{autocat}} </noinclude> [[Catégorie:Les moteurs de rendu des FPS en 2.5 D (livre)]] bfuurw37zzh0japm5gz6qs6ka3tupwg Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation 0 82429 745985 744719 2025-07-05T14:25:54Z Mewtow 31375 /* Les Virtual-8086 mode extensions */ 745985 wikitext text/x-wiki La virtualisation est l'ensemble des techniques qui permettent de faire tourner plusieurs systèmes d'exploitation en même temps. Le terme est polysémique, mais c'est la définition que nous allons utiliser pour ce qui nous intéresse. La virtualisation demande d'utiliser un logiciel dit '''hyperviseur''', qui permet de faire tourner plusieurs OS en même temps. Les hyperviseurs sont en quelque sorte situés sous le système d'exploitation. On peut les voir comme une sorte de sous-système d'exploitation, de système d'exploitation pour les systèmes d'exploitation. A ce propos, les OS virtualisés sont appelés des ''OS invités'', alors que l'hyperviseur est parfois appelé l'''OS hôte''. [[File:Diagramme ArchiHyperviseur.png|centre|vignette|upright=2|Différence entre système d'exploitation et hyperviseur.]] Les processeurs modernes intègrent des techniques pour accélérer la virtualisation. Les techniques en question sont assez variées, allant d'un niveau de privilège en plus des modes noyau/utilisateur à des modifications de la mémoire virtuelle, en passant à des modifications liées aux interruptions matérielles. Mais pour comprendre tout cela, il va falloir faire quelques explications sur la virtualisation elle-même. ==La virtualisation : généralités== Pour faire tourner plusieurs OS en même temps, l'hyperviseur recourt à de nombreux stratagèmes. Il doit partager le processeur, la RAM et les entrées-sorties entre plusieurs OS. Le partage de la RAM demande concrètement des modifications assez légères de la mémoire virtuelle, qu'on verra en temps voulu. Le partage du processeur est assez simple : les OS s'exécutent à tour de rôle sur le processeur, chacun pendant un temps défini, fixe. Une fois leur temps d'exécution passé, ils laissent la main à l'OS suivant. C'est l’hyperviseur qui s'occupe de tout cela, grâce à une interruption commandée à un ''timer''. Ce système de partage est une forme de '''multiplexage'''. A ce propos, il s'agit de la même solution que les OS utilisent pour faire tourner plusieurs programmes en même temps sur un processeur/cœur unique. La gestion des entrées-sorties demande d'utiliser des techniques d''''émulation''', plus complexes à expliquer. Un hyperviseur peut parfaitement simuler du matériel qui n'est pas installé sur l'ordinateur. Par exemple, il peut faire croire à un OS qu'une carte réseau obsolète, datant d'il y a 20 ans, est installée sur l'ordinateur, alors que ce n'est pas le cas. Les commandes envoyées par l'OS à cette carte réseau fictive sont en réalité traitées par une vraie carte réseau par l’hyperviseur. Pour cela, l’hyperviseur intercepte les commandes envoyées aux entrées-sorties, et les traduit en commandes compatibles avec les entrées-sorties réellement installées sur l'ordinateur. ===Les machines virtuelles=== L'exemple avec la carte réseau est un cas particulier, l'hyperviseur faisant beaucoup de choses dans le genre. L'hyperviseur peut faire croire à l'ordinateur qu'il a plus ou moins de RAM que ce qui est réellement installé, par exemple. L'hyperviseur implémente ce qu'on appelle des '''machines virtuelles'''. Il s'agit d'une sorte de faux matériel, simulé par un logiciel. Un logiciel qui s’exécute dans une machine virtuelle aura l'impression de s’exécuter sur un matériel et/ou un O.S différent du matériel sur lequel il est en train de s’exécuter. : Dans ce qui suit, nous parlerons de V.M (virtual machine), pour parler des machines virtuelles. [[File:VM-monitor-french.png|centre|vignette|upright=2|Machines virtuelles avec la virtualisation.]] Avec la virtualisation, plusieurs machines virtuelles sont gérées par l'hyperviseur, chacune étant réservée à un système d'exploitation. D'ailleurs, hyperviseurs sont parfois appelés des ''Virtual Machine Manager''. Nous utiliserons d'ailleurs l'abréviation VMM dans les schémas qui suivent. Il existe deux types d'hyperviseurs, qui sont nommés type 1 et type 2. Le premier type s'exécute directement sur le matériel, alors que le second est un logiciel qui s’exécute sur un OS normal. Pour ce qui nous concerne, la distinction n'est pas très importante. [[File:Ansatz der Systemvirtualisierung zur Schaffung virtueller Betriebsumgebungen.png|centre|vignette|upright=2.5|Comparaison des différentes techniques de virtualisation : sans virtualisation à gauche, virtualisation de type 1 au milieu, de type 2 à droite.]] La virtualisation est une des utilisations possibles, mais il y en a d'autres. La plus intéressante est celle des émulateurs. Ces derniers sont des logiciels qui permettent de simuler le fonctionnement d'anciens ordinateurs ou consoles de jeux. L'émulateur crée une machine virtuelle qui est réservée à un programme, à savoir le jeu à émuler. Il y a une différence de taille entre un émulateur et un hyperviseur. L'émulation émule une machine virtuelle totalement différente, alors que la virtualisation doit émuler les entrées-sorties mais pas le processeur. Avec un hyperviseur, le système d'exploitation s'exécute sur le processeur lui-même. Le code de l'OS est compatible avec le processeur de la machine, dans le sens où il est compilé pour le jeu d'instruction du processeur de la machine réelle. Les instructions de l'OS s'exécutent directement. Par contre, un émulateur exécute un jeu qui est programmé pour une machine dont le processeur est totalement différent. Le jeu d'instruction de la machine virtuelle et celui du vrai processeur n'est pas le même. L'émulation implique donc de traduire les instructions à exécuter dans la V.M par des instructions exécutables par le processeur. Ce n'est pas le cas avec la virtualisation, le jeu d'instruction étant le même. ===La méthode ''trap and emulate'' basique=== Pour être considéré comme un logiciel de virtualisation, un logiciel doit remplir trois critères : * L'équivalence : l'O.S virtualisé et les applications qui s’exécutent doivent se comporter comme s'ils étaient exécutés sur le matériel de base, sans virtualisation. * Le contrôle des ressources : tout accès au matériel par l'O.S virtualisé doit être intercepté par la machine virtuelle et intégralement pris en charge par l'hyperviseur. * L'efficacité : La grande partie des instructions machines doit s’exécuter directement sur le processeur, afin de garder des performances correctes. Ce critère n'est pas respecté par les émulateurs matériels, qui doivent simuler le jeu d'instruction du processeur émulé. Remplir ces trois critères est possible sous certaines conditions, établies par les théorèmes de Popek et Goldberg. Ces théorèmes se basent sur des hypothèses précises. De fait, la portée de ces théorèmes est limitée, notamment pour le critère de performance. Ils partent notamment du principe que l'ordinateur utilise la segmentation pour la mémoire virtuelle, et non la pagination. Il part aussi du principe que les interruptions ont un cout assez faible, qu'elles sont assez rares. Mais laissons ces détails de côté, le cœur de ces théorèmes repose sur une hypothèse simple : la présence de différents types d'instructions machines. Pour rappel, il faut distinguer les instructions privilégiées de celles qui ne le sont pas. Les instructions privilégiées ne peuvent s'exécuter que en mode noyau, les programmes en mode utilisateur ne peuvent pas les exécuter. Parmi les instructions privilégiées on peut distinguer un sous-groupe appelé les '''instructions systèmes'''. Le premier type regroupe les '''instructions d'accès aux entrées-sorties''', aussi appelées instructions sensibles à la configuration. Le second type est celui des '''instructions de configuration du processeur''', qui agissent sur les registres de contrôle du processeur, aussi appelées instructions sensibles au comportement. Elles servent notamment à gérer la mémoire virtuelle, mais pas que. La théorie de Popek et Goldberg dit qu'il est possible de virtualiser un O.S à une condition : que les instructions systèmes soient toutes des instructions privilégiées, c’est-à-dire exécutables seulement en mode noyau. Virtualiser un O.S demande simplement de le démarrer en mode utilisateur. Quand l'O.S fait un accès au matériel, il le fait via une instruction privilégiée. Vu que l'OS est en mode utilisateur, cela déclenche une exception matérielle, qui émule l'instruction privilégiée. L'hyperviseur n'est ni plus ni moins qu'un ensemble de routines d'interruptions, chaque routine simulant le fonctionnement du matériel émulé. Par exemple, un accès au disque dur sera émulé par une routine d'interruption, qui utilisera les appels systèmes fournit par l'OS pour accéder au disque dur réellement présent dans l'ordinateur. Cette méthode est souvent appelée la méthode ''trap and emulate''. [[File:Virtualisation avec la méthode trap-and-emulate.png|centre|vignette|upright=2.0|Virtualisation avec la méthode trap-and-emulate]] La méthode ''trap and emulate'' ne fonctionne que si certaines contraintes sont respectées. Un premier problème est que beaucoup de jeux d'instructions anciens ne respectent pas la règle "les instructions systèmes sont toutes privilégiées". Par exemple, ce n'est pas le cas sur les processeurs x86 32 bits. Sur ces CPU, les instructions qui manipulent les drapeaux d'interruption ne sont pas toutes des instructions privilégiées, idem pour les instructions qui manipulent les registres de segmentation, celles liées aux ''call gates'', etc. A cause de cela, il est impossible d'utiliser la méthode du ''trap and emulate''. La seule solution qui ne requiert pas de techniques matérielles est de traduire à la volée les instructions systèmes problématiques en appels systèmes équivalents, grâce à des techniques de '''réécriture de code'''. Enfin, certaines instructions dites '''sensibles au contexte''' ont un comportement différent entre le mode noyau et le mode utilisateur. En présence de telles instructions, la méthode ''trap and emulate'' ne fonctionne tout simplement pas. Grâce à ces instructions, le système d’exploitation ou un programme applicatif peut savoir s'il s'exécute en mode utilisateur ou noyau, ou hyperviseur, ou autre. La virtualisation impose l'usage de la mémoire virtuelle, sans quoi plusieurs OS ne peuvent pas se partager la même mémoire physique. De plus, il ne faut pas que la mémoire physique, non-virtuelle, puisse être adressée directement. Et cette contrainte est violée, par exemple sur les architectures MIPS qui exposent des portions de la mémoire physique dans certaines zones fixées à l'avance de la mémoire virtuelle. L'OS est compilé pour utiliser ces zones de mémoire pour accéder aux entrées-sorties mappées en mémoire, entre autres. En théorie, on peut passer outre le problème en marquant ces zones de mémoire comme inaccessibles, toute lecture/écriture à ces adresses déclenche alors une exception traitée par l'hyperviseur. Mais le cout en performance est alors trop important. Quelques hyperviseurs ont été conçus pour les architectures MIPS, dont le projet de recherche DISCO, mais ils ne fonctionnaient qu'avec des systèmes d'exploitation recompilés, de manière à passer outre ce problème. Les OS étaient recompilés afin de ne pas utiliser les zones mémoire problématiques. De plus, les OS étaient modifiés pour améliorer les performances en virtualisation. Les OS disposaient notamment d'appels systèmes spéciaux, appelés des ''hypercalls'', qui exécutaient des routines de l'hyperviseur directement. Les appels systèmes faisant appel à des instructions systèmes étaient ainsi remplacés par des appels système appelant directement l'hyperviseur. Le fait de modifier l'OS pour qu'il communique avec un hyperviseur, dont il a connaissance de l'existence, s'appelle la '''para-virtualisation'''. [[File:Virtualization - Para vs Full.png|centre|vignette|upright=2.5|Virtualization - Para vs Full]] ==La virtualisation du processeur== La virtualisation demande de partager le matériel entre plusieurs machines virtuelles. Précisément, il faut partager : le processeur, la mémoire RAM, les entrées-sorties. Les trois sont gérés différemment. Par exemple, la virtualisation des entrées-sorties est gérée par l’hyperviseur, parfois aidé par le ''chipset'' de la carte mère. Virtualiser des entrées-sorties demande d'émuler du matériel inexistant, mais aussi de dupliquer des entrées-sorties de manière à ce le matériel existe dans chaque VM. Partager la mémoire RAM entre plusieurs VM est assez simple avec la mémoire virtuelle, bien que cela demande quelques adaptations. Maintenant, voyons ce qu'il en est pour le processeur. ===Le niveau de privilège hyperviseur=== Sur certains CPU modernes, il existe un niveau de privilège appelé le '''niveau de privilège hyperviseur''' qui est utilisé pour les techniques de virtualisation. Le niveau de privilège hyperviseur est réservé à l’hyperviseur et il a des droits d'accès spécifiques. Il n'est cependant pas toujours activé. Par exemple, si aucun hyperviseur n'est installé sur la machine, le processeur dispose seulement des niveaux de privilège noyau et utilisateur, le mode noyau n'ayant alors aucune limitation précise. Mais quand le niveau de privilège hyperviseur est activé, une partie des manipulations est bloquée en mode noyau et n'est possible qu'en mode hyperviseur. Le fonctionnement se base sur la différence entre instruction privilégiée et instruction système. Les instructions privilégiées peuvent s'exécuter en niveau noyau, alors que les instructions systèmes ne peuvent s'exécuter qu'en niveau hyperviseur. L'idée est que quand le noyau d'un OS exécute une instruction système, une exception matérielle est levée. L'exception bascule en mode hyperviseur et laisse la main à une routine de l'hyperviseur. L'hyperviseur fait alors des manipulations précise pour que l'instruction système donne le même résultat que si elle avait été exécutée par l'ordinateur simulé par la machine virtuelle. [[File:Virtualisation avec un mode hyperviseur.png|centre|vignette|upright=2|Virtualisation avec un mode hyperviseur.]] Il est ainsi possible d'émuler des entrées-sorties avec un cout en performance assez léger. Précisément, ce mode hyperviseur améliore les performances de la méthode du ''trap-and-emulate''. La méthode ''trap-and-emulate'' basique exécute une exception matérielle pour toute instruction privilégiée, qu'elle soit une instruction système ou non. Mais avec le niveau de privilège hyperviseur, seules les instructions systèmes déclenchent une exception, pas les instructions privilégiées non-système. Les performances sont donc un peu meilleures, pour un résultat identique. Après tout, les entrées-sorties et la configuration du processeur suffisent à émuler une machine virtuelle, les autres instructions noyau ne le sont pas. Sur les processeurs ARM, il est possible de configurer quelles instructions sont détournées vers le mode hyperviseur et celles qui restent en mode noyau. En clair, on peut configurer quelles sont les instructions systèmes et celles qui sont simplement privilégiées. Et il en est de même pour les interruptions : on peut configurer si elles exécutent la routine de l'OS normal en mode noyau, ou si elles déclenchent une exception matérielle qui redirige vers une routine de l’hyperviseur. En l'absence d'hyperviseur, toutes les interruptions redirigent vers la routine de l'OS normale, vers le mode noyau. Il faut noter que le mode hyperviseur n'est compatible qu'avec les hyperviseurs de type 1, à savoir ceux qui s'exécutent directement sur le matériel. Par contre, elle n'est pas compatible avec les hyperviseurs de type 2, qui sont des logiciels qui s'exécutent comme tout autre logiciel, au-dessus d'un système d'exploitation sous-jacent. ===L'Intel VT-X et l'AMD-V=== Les processeurs ARM de version v8 et plus incorporent un mode hyperviseur, mais pas les processeurs x86. À la place, ils incorporent des technologies alternatives nommées Intel VT-X ou l'AMD-V. Les deux ajoutent de nouvelles instructions pour gérer l'entrée et la sortie d'un mode réservé à l’hyperviseur. Mais ce mode réservé à l'hyperviseur n'est pas un niveau de privilège comme l'est le mode hyperviseur. L'Intel VT-X et l'AMD-V dupliquent le processeur en deux modes de fonctionnement : un mode racine pour l'hyperviseur, un mode non-racine pour l'OS et les applications. Fait important : les niveaux de privilège sont dupliqués eux aussi ! Par exemple, il y a un mode noyau racine et un mode noyau non-racine, idem pour le mode utilisateur, idem pour le mode système (pour le BIOS/UEFI). De même, les modes réel, protégé, v8086 ou autres, sont eux aussi dupliqués en un exemplaire racine et un exemplaire non-racine. L'avantage est que les systèmes d'exploitation virtualisés s'exécutent bel et bien en mode noyau natif, l'hyperviseur a à sa disposition un mode noyau séparé. D'ailleurs, les deux modes ont des registres d'interruption différents. Le mode racine et le mode non-racine ont chacun leurs espaces d'adressage séparés de 64 bit, avec leur propre table des pages. Et cela demande des adaptations au niveau de la TLB. La transition entre mode racine et non-racine se fait lorsque le processeur exécute une instruction système ou lors de certaines interruptions. Au minimum, toute exécution d'une instruction système fait commuter le processeur mode racine et lance l'exécution des routines de l’hyperviseur adéquates. Les interruptions matérielles et exceptions font aussi passer le CPU en mode racine, afin que l’hyperviseur puisse gérer le matériel. De plus, afin de gérer le partage de la mémoire entre OS, certains défauts de page déclenchent l'entrée en mode racine. Les ''hypercalls'' de la para-virtualisation sont supportés grâce à aux instructions ''vmcall'' et ''vmresume'' qui permettent respectivement d'appeler une routine de l’hyperviseur ou d'en sortir. La transition demande de sauvegarder/restaurer les registres du processeur, comme avec les interruptions. Mais cette sauvegarde est réalisée automatiquement par le processeur, elle n'est pas faite par les routines de l'hyperviseur. L’implémentation de cette sauvegarde/restauration se fait surtout via le microcode du processeur, car elle demande beaucoup d'étapes. Elle est en conséquence très lente. Le processeur sauvegarde l'état de chaque machine virtuelle en mémoire RAM, dans une structure de données appelée la ''Virtual Machine Control Structure'' (VMCS). Elle mémorise surtout les registres du processeur à l'instant t. Lorsque le processeur démarre l'exécution d'une VM sur le processeur, cette VMCS est recopiée dans les registres pour rétablir la VM à l'endroit où elle s'était arrêtée. Lorsque la VM est interrompue et doit laisser sa place à l'hyperviseur, les registres et l'état du processeur sont sauvegardés dans la VMCS adéquate. ==La virtualisation de la mémoire : mémoire virtuelle et MMU== Avec la virtualisation, les différentes machines virtuelles, les différents OS doivent se partager la mémoire physique, en plus d'être isolés les uns des autres. L'idée est d'utiliser la mémoire virtuelle pour cela. L'espace d'adressage physique vu par chaque OS est en réalité un espace d'adressage fictif, qui ne correspond pas à la mémoire physique. Les adresses physiques manipulées par l'OS sont en réalité des adresses intermédiaires entre les adresses physiques liées à la RAM, et les adresses virtuelles vues par les processus. Pour les distinguer, nous parlerons d'adresses physiques de l'hôte pour parler des adresses de la RAM, et des adresses physiques invitées pour parler des adresses manipulées par les OS virtualisés. Sans accélération matérielle, la traduction des adresses physiques invitées en adresses hôte est réalisée par une seconde table des pages, appelée la ''shadow page table'', ce qui donnerait '''table des pages cachée''' en français. La table des pages cachée est prise en charge par l'hyperviseur. Toute modification de la table des pages cachée est réalisée par l'hyperviseur, les OS ne savent même pas qu'elle existe. [[File:Shadowpagetables.png|centre|vignette|upright=2|Table des pages cachée.]] ===La MMU et la virtualisation : les tables des pages emboitées=== Une autre solution demande un support matériel des tables des pages emboitées, à savoir qu'il y a un arbre de table des pages, chaque consultation de la première table des pages renvoie vers une seconde, qui renvoie vers une troisième, et ainsi de suite jusqu'à tomber sur la table des pages finale qui renvoie l'adresse physique réelle. L'idée est l'utiliser une seule table des pages, mais d'ajouter un ou deux niveaux supplémentaires. Pour l'exemple, prenons le cas des processeurs x86. Sans virtualisation, l'OS utilise une table des pages de 4 niveaux. Avec, la table des pages a un niveau en plus, qui sont ajoutés à la fin de la dernière table des pages normale. Les niveaux ajoutés s'occupent de la traduction des adresses physiques invitées en adresses physiques hôte. On parle alors de '''table des pages étendues''' pour désigner ce nouveau format de table des pages conçu pour la virtualisation. Il faut que le processeur soit modifié de manière à parcourir automatiquement les niveaux ajoutés, ce qui demande quelques modifications de la TLB et du ''page table walker''. Les modifications en question ne font que modifier le format normal de la table des pages, et sont donc assez triviales. Elles ont été implémentées sur les processeurs AMD et Intel. AMD a introduit les tables des pages étendues sur ses processeurs Opteron, destinés aux serveurs, avec sa technologie ''Rapid Virtualization Indexing''. Intel, quant à lui, a introduit la technologie sur les processeurs i3, i5 et i7, sous le nom ''Extended Page Tables''. Les processeurs ARM ne sont pas en reste avec la technologie ''Stage-2 page-tables'', qui est utilisée en mode hyperviseur. ===La virtualisation de l'IO-MMU=== Si la MMU du processeur est modifiée pour gérer des tables des pages étendues, il en est de même pour les IO-MMU des périphériques et contrôleurs DMA.Les périphériques doivent idéalement intégrer une IO-MMU pour faciliter la virtualisation. La raison est globalement la même que pour le partage de la mémoire. Les pilotes de périphériques utilisent des adresses qui sont des adresses physiques sans virtualisation, mais qui deviennent des adresses virtuelles avec. Quand le pilote de périphérique configure un contrôleur DMA, pour transférer des données de la RAM vers un périphérique, il utilisera des adresses virtuelles qu'il croit physique pour adresser les données en RAM. Pour éviter tout problème, le contrôleur DMA doit traduire les adresses qu'il reçoit en adresses physiques. Pour cela, il y a besoin d'une IO-MMU intégrée au contrôleur DMA, qui est configurée par l'hyperviseur. Toute IO-MMU a sa propre table des pages et l'hyperviseur configure les table des pages pour chaque périphérique. Ainsi, le pilote de périphérique manipule des adresses virtuelles, qui sont traduites en adresses physiques directement par le matériel lui-même, sans intervention logicielle. Pour gérer la virtualisation, on fait la même chose qu'avec une table des pages emboitée habituelle : on l'étend en ajoutant des niveaux. L'IO-MMU peut fonctionner dans un mode normal, sans virtualisation, où les adresses virtuelles reçues du ''driver'' sont traduite avec une table des pages normale, non-emboitée. Mais elle a aussi un mode virtualisation qui utilise des tables de pages étendues. ==La virtualisation des entrées-sorties== Virtualiser les entrées-sorties est simple sur le principe. Un OS communique avec le matériel soit via des ports IO, soit avec des entrées-sorties mappées en mémoire. Le périphérique répond avec des interruptions ou via des transferts DMA. Virtualiser les périphériques demande alors d'émuler les ports IO, les entrées-sorties mappées en mémoire, le DMA et les interruptions. ===La virtualisation logicielle des interruptions=== Émuler les ports IO est assez simple, vu que l'OS lit ou écrit dedans grâce à des instructions IO spécialisées. Vu que ce sont des instructions système, la méthode ''trap and emulate'' suffit. Pour les entrées-sorties mappées en mémoire, l'hyperviseur a juste à marquer les adresses mémoires concernées comme étant réservées/non-allouées/autre. Tout accès à ces adresses lèvera une exception matérielle d'accès mémoire interdit, que l’hyperviseur intercepte et gère via ''trap and emulate''. L'émulation du DMA est triviale, vu que l'hyperviseur a accès direct à celui-ci, sans compter que l'usage d'une IO-MMU résout beaucoup de problèmes. La gestion des interruptions matérielles, les fameuses IRQ, est quant à elle plus complexe. Les interruptions matérielles ne sont pas à prendre en compte pour toutes les machines virtuelles. Par exemple, si une machine virtuelle n'a pas de carte graphique, pas besoin qu'elle prenne en compte les interruptions provenant de la carte graphique. La gestion des interruptions matérielles n'est pas la même si l'ordinateur grée des cartes virtuelles ou s'il se débrouille avec une carte physique unique. Lors d'une interruption matérielle, le processeur exécute la routine adéquate de l'hyperviseur. Celle-ci enregistre qu'il y a eu une IRQ et fait quelques traitements préliminaires. Ensuite, elle laisse la main au système d'exploitation concerné, qui exécute alors sa routine d'interruption. Une fois la routine de l'OS terminée, l'OS dit au contrôleur d'interruption qu'il a terminé son travail. Mais cela demande d'interagir avec le contrôleur d'interruption, ce qui déclenche une exception qui appelle l'hyperviseur. L'hyperviseur signale au contrôleur d'interruption que l'interruption matérielle a été traitée. Il rend alors définitivement la main au système d'exploitation. Le processus complet demande donc plusieurs changements entre mode hyperviseur et OS, ce qui est assez couteux en performances. Vu que le matériel simulé varie d'une machine virtuelle à l'autre, chaque machine virtuelle a son propre vecteur d'interruption. Par exemple, si une machine virtuelle n'a pas de carte graphique son vecteur d'interruption ne pointera pas vers les routines d'interruption d'un quelconque GPU. L'hyperviseur gère les différents vecteurs d'interruption de chaque VM et traduit les interruptions reçues en interruptions destinées aux VM/OS. Si la méthode ''trap and emulate'' fonctionne, ses performances ne sont cependant pas forcément au rendez-vous. Tous les matériels ne se prêtent pas tous bien à la virtualisation, surtout les périphériques anciens. Pour éliminer une partie de ces problèmes, il existe différentes techniques, accélérées en matériel ou non. Elles permettent aux machines virtuelles de communiquer directement avec les périphériques, sans passer par l'hyperviseur. ===La virtualisation des périphériques avec l'affectation directe=== Virtualiser les entrées-sorties avec de bonnes performances est plus complexe. En pratique, cela demande une intervention du matériel. Le ''chipset'' de la carte mère, les différents contrôleurs d'interruption et bien d'autres circuits doivent être modifiés. Diverses techniques permettent de faciliter le partage des entrées-sorties entre machines virtuelles. La première est l''''affectation directe''', qui alloue un périphérique à une machine virtuelle et pas aux autres. Par exemple, il est possible d'assigner la carte graphique à une machine virtuelle tournant sur Windows, mais les autres machines virtuelles ne verront même pas la carte graphique. Même l'hyperviseur n'a pas accès directement à ce matériel. L'affectation directe est très utile sur les serveurs, qui disposent souvent de plusieurs cartes réseaux et peuvent en assigner une à chaque machine virtuelle. Mais dans la plupart des cas, elle ne marche pas. De plus, sur les périphériques sans IO-MMU, elle ouvre la porte à des attaques DMA, où une machine virtuelle accède à la mémoire physique de la machine en configurant le contrôleur DMA de son périphérique assigné. L'affectation directe est certes limitée, mais elle se marie bien avec certaines de virtualisation matérielles, intégrées dans de nombreux périphériques. Il existe des périphériques qui sont capables de se virtualiser tout seuls, à savoir qu'ils peuvent se dédoubler en plusieurs '''périphériques virtuels'''. Par exemple, prenons une carte réseau avec cette propriété. Il n'y a qu'une seule carte réseau dans l'ordinateur, mais elle peut donner l'illusion qu'il y en a 8-16 d'installés dans l'ordinateur. Il faut alors faire la différence entre la carte réseau physique et les 8-16 cartes réseau virtuelles. L'idée est d'utiliser l'affectation directe, chaque machine virtuelle/OS ayant une carte réseau virtuelle d'affectée, avec affectation directe. [[File:Virtualisation matérielle des périphériques.png|centre|vignette|upright=2|Virtualisation matérielle des périphériques]] Pour les périphériques PCI-Express, le fait de se dupliquer en plusieurs périphériques virtuels est permis par la technologie '''''Single-root input/output virtualization''''', abrévié en SRIOV. Elle est beaucoup, utilisée sur les cartes réseaux, pour plusieurs raisons. Déjà, ce sont des périphériques beaucoup utilisés sur les serveurs, qui utilisent beaucoup la virtualisation. Dupliquer des cartes réseaux et utiliser l'affectation directe rend la configuration des serveurs bien plus simple. De plus, la plupart des cartes réseaux sont sous-utilisées, même par les serveurs. Une carte réseau est souvent utilisée à environ 10% de ses capacités par une VM unique, ce qui fait qu'utiliser 10 cartes réseaux virtuelles permet d'utiliser les capacités de la carte réseau à 100%. Il est possible de faire une analogie entre les processeurs multithreadés et les périphériques virtuels. Un processeur multithreadé est dupliqué en plusieurs processeurs virtuels, un périphérique virtualisé est dupliqué en plusieurs périphériques virtuels. L'implémentation des deux techniques est similaire sur le principe, mais les détails varient selon qu'on parle d'une carte réseau, d'une carte graphique, d'une carte son, etc. Pour gérer plusieurs périphériques virtuels, le périphérique physique contient plusieurs copies de ses registres de commande/données, plusieurs files de commandes, etc. De plus, le périphérique physique contient divers circuits d'arbitrage, qui gèrent comment le matériel est utilisé. Ils donnent accès à tour de rôle à chaque VM aux ressources non-dupliquées. [[File:Implémentation d'une carte réseau gérant plusieurs cartes réseaux virtuelles.png|centre|vignette|upright=2|Implémentation d'une carte réseau gérant plusieurs cartes réseaux virtuelles]] Dans le cas le plus simple, le matériel traite les commandes provenant des différentes VM dans l'ordre d'arrivée, une par une, il n'y a pas d'arbitrage pour éviter qu'une VM monopolise le matériel. Plus évolué, le matériel peut faire de l'affectation au tour par tour, en traitant chaque VM dans l'ordre durant un certain temps. Le matériel peut aussi utiliser des algorithmes d'ordonnancement/répartition plus complexes. Par exemple, les cartes graphiques modernes utilisent des algorithmes de répartition/ordonnancement accélérés en matériel, implémentés dans le GPU lui-même. ===La virtualisation des interruptions=== La gestion des interruptions matérielles peut aussi être accélérée en matériel, en complément des techniques de périphériques virtuels vues plus haut. Par exemple, il est possible de gérer des ''exitless interrupts'', qui ne passent pas du tout par l'hyperviseur. Mais cela demande d'utiliser l'affectation directe, en complément de l'usage de périphériques virtuels. Tout périphérique virtuel émet des interruptions distinctes des autres périphérique virtuel. Pour distinguer les interruptions provenant de cartes virtuelles de celles provenant de cartes physiques, on les désigne sous le terme d''''interruptions virtuelles'''. Une interruption virtuelle est destinée à une seule machine virtuelle : celle à laquelle est assignée la carte virtuelle. Les autres machines virtuelles ne reçoivent pas ces interruptions. Les interruptions virtuelles ne sont pas traitées par l'hyperviseur, seulement par l'OS de la machine virtuelle assignée. Une subtilité a lieu sur les processeurs à plusieurs cœurs. Il est possible d'assigner un cœur à chaque machine virtuelle, possiblement plusieurs. Par exemple, un processeur octo-coeur peut exécuter 8 machines virtuelles simultanément. Avec l'affectation directe ou à tour de rôle, l'interruption matérielle est donc destinée à une machine virtuelle, donc à un cœur. L'IRQ doit donc être redirigée vers un cœur bien précis et ne pas être envoyée aux autres. Les contrôleurs d'interruption modernes déterminent à quelles machines virtuelles sont destinées telle ou telle interruption, et peuvent leur envoyer directement, sans passer par l'hyperviseur. Grâce à cela, l'affectation directe à tour de rôle ont de bonnes performances. En plus de ce support des interruptions virtuelles, le contrôleur d'interruption peut aussi être virtualisé, à savoir être dupliqué en plusieurs '''contrôleurs d'interruption virtuels'''. Sur les systèmes à processeur x86, le contrôleur d'interruption virtualisé est l'APIC (''Advanced Programmable Interrupt Controller''). Diverses technologies de vAPIC, aussi dites d'APIC virtualisé, permettent à chaque machine virtuelle d'avoir une copie virtuelle de l'APIC. Pour ce faire, tous les registres de l'APIC sont dupliqués en autant d'exemplaires que d'APIC virtuels supportés. Il existe un équivalent sur les processeurs ARM, où le contrôleur d'interruption est nommé le ''Generic Interrupt Controller'' (GIC) et peut aussi être virtualisé. La virtualisation de l'APIC permet d'éviter d'avoir à passer par l'hyperviseur pour gérer les interruptions. Par exemple, quand un OS veut prévenir qu'il a fini de traiter une interruption, il doit communiquer avec le contrôleur d'interruption. Sans virtualisation du contrôleur d'interruption, cela demande de passer par l'intermédiaire de l'hyperviseur. Mais s'il est virtualisé, l'OS peut communiquer directement avec le contrôleur d'interruption virtuel qui lui est associé, sans que l'hyperviseur n'ait à faire quoique ce soit. De plus, la virtualisation du contrôleur d'interruption permet de gérer des interruptions inter-processeurs dites postées, qui ne font pas appel à l'hyperviseur, ainsi que des interruptions virtuelles émises par les IO-MMU. Sur les plateformes ARM, les ''timers'' sont aussi virtualisés. ==Annexe : le mode virtuel 8086 des premiers CPU Intel== Les premiers processeurs x86 étaient rudimentaires. Le 8086 utilisait une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, dont le but était d'adresser plus de 64 kibioctets de mémoire avec un processeur 16 bits. Sur le processeur 286, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'espace d'adressage en mode protégé est passé de 24 bits sur le CPU 286, à 32 bits sur le 386 et les CPU suivants. Pour bien faire la différence, la segmentation du 8086 fut appelée le mode réel, et la nouvelle forme de segmentation fut appelée le mode protégé. Les programmes conçus pour le mode réel ne pouvaient pas s'exécuter en mode protégé. En clair, tous les programmes conçus pour le 8086 devaient fonctionner en mode réel, qui était supporté sur le 286 et les processeurs suivant. Pour corriger les problèmes observés sur le 286, le 386 a ajouté un '''mode 8086 virtuel''', une technique de virtualisation qui permet à des programmes de s'exécuter en mode réel dans une machine virtuelle dédiée appelée la '''VM V86'''. Notez que nous utiliserons l'abréviation V86 pour parler du mode virtuel 8086, ainsi que de tout ce qui est lié à ce mode. ===La virtualisation du DOS et la mémoire étendue=== Utiliser le mode V86 demande d'avoir un programme 8086 à lancer, mais aussi d'utiliser un '''hyperviseur V86'''. L’hyperviseur V86 est un véritable hyperviseur, qui s’exécute en mode noyau, exécute des routines d'interruption, gère les exceptions matérielles, etc. Il réside en mémoire dans une zone non-adressable en mode réel, mais accessible en mode protégé, celui-ci permettant d'adresser plus de RAM. L'hyperviseur est forcément exécuté en mode protégé. Le système d'exploitation DOS s'exécutait en mode réel, ce qui fait qu'il pouvait être émulé par le mode V86. Il était ainsi possible de lancer une ou plusieurs sessions DOS à partir d'un système d'exploitation multitâche comme Windows. Windows. Beaucoup de personnes nées avant les années 2000 ont sans doute profité de cette possibilité pour lancer des applications DOS sous Windows. Les applications étaient en réalité lancées dans une machine virtuelle grâce au mode V86. Windows implémentait un hyperviseur V86 de type 2, à savoir que c'était un logiciel qui s'exécutait sur un OS sous-jacent, ici [[File:Hyperviseur.svg|centre|vignette|upright=2|Hyperviseur]] Les applications DOS dans une VM V86 ne peuvent pas adresser plus d'un mébioctet de mémoire. L'ordinateur peut cependant avoir plus de mémoire RAM, notamment pour gérer l'hyperviseur V86. Diverses techniques permettaient aux applications DOS d'utiliser la mémoire au-delà du mébioctet, appelée la '''mémoire étendue'''. Les logiciels DOS accédaient à la mémoire étendue en passant par un intermédiaire logiciel, qui lui-même communiquait avec l'hyperviseur V86. L'intermédiaire est appelé le ''Extended Memory Manager'' (EMM), et il est concrètement implémenté par un driver sur DOS (HIMEM.SYS). Les applications DOS ne pouvaient pas adresser la mémoire étendue, mais pouvaient échanger des données avec l'EMM. Les logiciels peuvent ainsi déplacer des données dans la mémoire étendue pour les garder au chaud, puis les rapatrier dans la mémoire conventionnelle quand ils en avaient besoin. L'intermédiaire ''Extended Memory Manager'' s'occupe d’échanger des données entre mémoire conventionnelle et mémoire étendue. Pour cela, il switche entre mode réel et protégé à la demande, quand il doit lire ou écrire en mémoire étendue. Il ne faut pas confondre mémoire étendue et ''expanded memory''. Pour rappel, l'''expanded memory'' est un système de commutation de banque, qui autorise un va-et-vient entre une carte d'extension et une page de 64 kibioctets mappé en mémoire haute. Elle fonctionne sans mode protégé, sans virtualisation, sans mode V86. La mémoire étendue ne gère pas de commutation de banque et demande que la RAM en plus soit installée dans l'ordinateur, pas sur une carte d'extension. Par contre, il est possible d'émuler l'''expanded memory'' sans carte d'extension, en utilisant la mémoire étendue. Quelques ''chipsets'' de carte mère intégraient des techniques cela. Une émulation logicielle était aussi possible. L'émulation logicielle se basait sur une réécriture de l'interruption 67h utilisée pour adresser la technologie ''expanded memory''. L'hyperviseur V86 pouvait s'en charger, il avait juste à réécrire son allocateur de mémoire pour gérer cette interruption et quelques autres détails. ===Le fonctionnement du mode virtuel 8086 de base=== En mode V86, la segmentation du mode protégé est désactivée, seule la segmentation du mode réel est utilisée. Il y a quelques subtilités liées à la ligne A20 du bus d'adresse, déjà abordées auparavant dans ce cours. Sur les CPU 286 et ultérieurs, le processeur peut adresser 1 mébioctet (2^20 adresses), plus 64 kibioctets qui ne sont pas adressables sur le 8086. Le tout permet donc d'adresser les adresses allant de 0 à 0x010FFEFH. Et ces adresses sont utilisées pour les programmes en mode réel. Les adresses au-delà de l'adresse 0x010FFEFH sont typiquement le lieu de résidence de l’hyperviseur en RAM. Par contre, la pagination peut être activée par l’hyperviseur, afin d’exécuter plusieurs logiciels en mode réel simultanément. La mémoire virtuelle par pagination peut aussi être utile si l'ordinateur a peu de mémoire RAM, pas assez pour faire tourner le logiciel : l'espace d'adressage vu par le logiciel est un espace virtuel de grande taille, ce qui permet de lancer le logiciel, au prix de performances dégradées. Enfin, la gestion des entrées-sorties mappées en mémoire est aussi simplifiée. De plus, cela permettait d'adresser plus de mémoire RAM grâce aux adresses plus longues du mode protégé. Le processeur est configuré en mode V86, le bit VM du registre d'état spécifique est mis à 1. Le processeur utilise ce bit lorsqu'une instruction utilise les registres de segments, afin de savoir comment calculer les adresses, le calcul n'étant pas le même en mode réel et en mode protégé. Quelques instructions machines dépendent aussi de la valeur de ce bit, mais cela est traité au décodage de l'instruction. Il faut noter que dans le mode 8086 virtuel, les programmes peuvent utiliser les registres ajoutés sur le 386 et ultérieurs. Par exemple, le 8086 n'a que 4 registres de segment, alors que le 286 en a 6. Les programmes en mode 8086 virtuel peuvent utiliser les deux registres de segment supplémentaires. Il en est de même pour d'autres registres ajoutés par le 286, comme des registres de contrôle, des registres de debug, et quelques autres. Il en est de même pour les instructions ajoutées par le 286, le 386 et ultérieur, qui sont exécutables en mode virtuel 8086. Et elles sont nombreuses. La compatibilité n'était pas parfaite, il y avait quelques petites différences entre ce mode V86 et le mode réel du 8086, idem avec le mode réel du 286. Mais la grande majorité des applications n'avait aucun problème. Les problèmes étaient concentrés sur quelques instructions précises, notamment celles avec un préfixe LOCK. ===Les ''Virtual-8086 mode extensions''=== A partir du processeur Pentium, les processeurs x86 ont introduit des optimisations du mode V86, afin de rendre la virtualisation plus rapide. L'ensemble de ces optimisations est regroupé sous le terme de '''''Virtual-8086 mode extensions''''', abrévié en VME. Les optimisations du VMA étaient, pour certaines, utiles au-delà de la virtualisation et étaient activables indépendamment du reste du VME. Le VME introduisait des optimisations quant au traitement des interruptions, à savoir la gestion des interruptions virtuelles. De plus, le VME modifie la gestion de l'''interrupt flag'' du registre d'état. Pour rappel, ce bit permet d'activer ou de désactiver les interruptions masquables. Modifier le bit ''interrupt flag'' permettait de désactiver les interruptions masquables ou au contraire de les activer. Il se trouve que ce bit était accesible par les programmes exécutés en mode réel, qui pouvaient en faire ce qu'ils voulaient. Le mode réel n'étant pas prévu pour la multi-programmation, ce n'était pas un problème. Mais en mode V86, toute modification de ce bit se répercute sur les autres VM en mode V86. Pour éviter les problèmes, le VME a ajouté de quoi virtualiser cet ''interrupt flag'', avec une copie par machine virtuelle V86. Chaque programme modifiait sa propre copie de l'''interrupt flag'' sans altérer celle des autres programmes exécutés en mode V86, et surtout sans déclencher une exception matérielle gérée par l'hyperviseur. Bien qu'elles aient été introduites sur les processeurs Pentium, elles n'ont réellement été rendues publiques qu'après la sortie des processeurs de microarchitecture P6. Avant d'être rendue publique, la documentation du VME était une annexe de la documentation officielle, la fameuse annexe H. Elle était mentionnée dans la documentation officielle, mais était indisponible au grand public, seules quelques entreprises sous NDA y avait accès. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Exemples de microarchitectures CPU : le cas du x86 | prevText=Exemples de microarchitectures CPU : le cas du x86 | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 7ma2v2hgt90yeerqs9l7kpf273aorvn 745998 745985 2025-07-05T14:58:04Z Mewtow 31375 /* Les Virtual-8086 mode extensions */ 745998 wikitext text/x-wiki La virtualisation est l'ensemble des techniques qui permettent de faire tourner plusieurs systèmes d'exploitation en même temps. Le terme est polysémique, mais c'est la définition que nous allons utiliser pour ce qui nous intéresse. La virtualisation demande d'utiliser un logiciel dit '''hyperviseur''', qui permet de faire tourner plusieurs OS en même temps. Les hyperviseurs sont en quelque sorte situés sous le système d'exploitation. On peut les voir comme une sorte de sous-système d'exploitation, de système d'exploitation pour les systèmes d'exploitation. A ce propos, les OS virtualisés sont appelés des ''OS invités'', alors que l'hyperviseur est parfois appelé l'''OS hôte''. [[File:Diagramme ArchiHyperviseur.png|centre|vignette|upright=2|Différence entre système d'exploitation et hyperviseur.]] Les processeurs modernes intègrent des techniques pour accélérer la virtualisation. Les techniques en question sont assez variées, allant d'un niveau de privilège en plus des modes noyau/utilisateur à des modifications de la mémoire virtuelle, en passant à des modifications liées aux interruptions matérielles. Mais pour comprendre tout cela, il va falloir faire quelques explications sur la virtualisation elle-même. ==La virtualisation : généralités== Pour faire tourner plusieurs OS en même temps, l'hyperviseur recourt à de nombreux stratagèmes. Il doit partager le processeur, la RAM et les entrées-sorties entre plusieurs OS. Le partage de la RAM demande concrètement des modifications assez légères de la mémoire virtuelle, qu'on verra en temps voulu. Le partage du processeur est assez simple : les OS s'exécutent à tour de rôle sur le processeur, chacun pendant un temps défini, fixe. Une fois leur temps d'exécution passé, ils laissent la main à l'OS suivant. C'est l’hyperviseur qui s'occupe de tout cela, grâce à une interruption commandée à un ''timer''. Ce système de partage est une forme de '''multiplexage'''. A ce propos, il s'agit de la même solution que les OS utilisent pour faire tourner plusieurs programmes en même temps sur un processeur/cœur unique. La gestion des entrées-sorties demande d'utiliser des techniques d''''émulation''', plus complexes à expliquer. Un hyperviseur peut parfaitement simuler du matériel qui n'est pas installé sur l'ordinateur. Par exemple, il peut faire croire à un OS qu'une carte réseau obsolète, datant d'il y a 20 ans, est installée sur l'ordinateur, alors que ce n'est pas le cas. Les commandes envoyées par l'OS à cette carte réseau fictive sont en réalité traitées par une vraie carte réseau par l’hyperviseur. Pour cela, l’hyperviseur intercepte les commandes envoyées aux entrées-sorties, et les traduit en commandes compatibles avec les entrées-sorties réellement installées sur l'ordinateur. ===Les machines virtuelles=== L'exemple avec la carte réseau est un cas particulier, l'hyperviseur faisant beaucoup de choses dans le genre. L'hyperviseur peut faire croire à l'ordinateur qu'il a plus ou moins de RAM que ce qui est réellement installé, par exemple. L'hyperviseur implémente ce qu'on appelle des '''machines virtuelles'''. Il s'agit d'une sorte de faux matériel, simulé par un logiciel. Un logiciel qui s’exécute dans une machine virtuelle aura l'impression de s’exécuter sur un matériel et/ou un O.S différent du matériel sur lequel il est en train de s’exécuter. : Dans ce qui suit, nous parlerons de V.M (virtual machine), pour parler des machines virtuelles. [[File:VM-monitor-french.png|centre|vignette|upright=2|Machines virtuelles avec la virtualisation.]] Avec la virtualisation, plusieurs machines virtuelles sont gérées par l'hyperviseur, chacune étant réservée à un système d'exploitation. D'ailleurs, hyperviseurs sont parfois appelés des ''Virtual Machine Manager''. Nous utiliserons d'ailleurs l'abréviation VMM dans les schémas qui suivent. Il existe deux types d'hyperviseurs, qui sont nommés type 1 et type 2. Le premier type s'exécute directement sur le matériel, alors que le second est un logiciel qui s’exécute sur un OS normal. Pour ce qui nous concerne, la distinction n'est pas très importante. [[File:Ansatz der Systemvirtualisierung zur Schaffung virtueller Betriebsumgebungen.png|centre|vignette|upright=2.5|Comparaison des différentes techniques de virtualisation : sans virtualisation à gauche, virtualisation de type 1 au milieu, de type 2 à droite.]] La virtualisation est une des utilisations possibles, mais il y en a d'autres. La plus intéressante est celle des émulateurs. Ces derniers sont des logiciels qui permettent de simuler le fonctionnement d'anciens ordinateurs ou consoles de jeux. L'émulateur crée une machine virtuelle qui est réservée à un programme, à savoir le jeu à émuler. Il y a une différence de taille entre un émulateur et un hyperviseur. L'émulation émule une machine virtuelle totalement différente, alors que la virtualisation doit émuler les entrées-sorties mais pas le processeur. Avec un hyperviseur, le système d'exploitation s'exécute sur le processeur lui-même. Le code de l'OS est compatible avec le processeur de la machine, dans le sens où il est compilé pour le jeu d'instruction du processeur de la machine réelle. Les instructions de l'OS s'exécutent directement. Par contre, un émulateur exécute un jeu qui est programmé pour une machine dont le processeur est totalement différent. Le jeu d'instruction de la machine virtuelle et celui du vrai processeur n'est pas le même. L'émulation implique donc de traduire les instructions à exécuter dans la V.M par des instructions exécutables par le processeur. Ce n'est pas le cas avec la virtualisation, le jeu d'instruction étant le même. ===La méthode ''trap and emulate'' basique=== Pour être considéré comme un logiciel de virtualisation, un logiciel doit remplir trois critères : * L'équivalence : l'O.S virtualisé et les applications qui s’exécutent doivent se comporter comme s'ils étaient exécutés sur le matériel de base, sans virtualisation. * Le contrôle des ressources : tout accès au matériel par l'O.S virtualisé doit être intercepté par la machine virtuelle et intégralement pris en charge par l'hyperviseur. * L'efficacité : La grande partie des instructions machines doit s’exécuter directement sur le processeur, afin de garder des performances correctes. Ce critère n'est pas respecté par les émulateurs matériels, qui doivent simuler le jeu d'instruction du processeur émulé. Remplir ces trois critères est possible sous certaines conditions, établies par les théorèmes de Popek et Goldberg. Ces théorèmes se basent sur des hypothèses précises. De fait, la portée de ces théorèmes est limitée, notamment pour le critère de performance. Ils partent notamment du principe que l'ordinateur utilise la segmentation pour la mémoire virtuelle, et non la pagination. Il part aussi du principe que les interruptions ont un cout assez faible, qu'elles sont assez rares. Mais laissons ces détails de côté, le cœur de ces théorèmes repose sur une hypothèse simple : la présence de différents types d'instructions machines. Pour rappel, il faut distinguer les instructions privilégiées de celles qui ne le sont pas. Les instructions privilégiées ne peuvent s'exécuter que en mode noyau, les programmes en mode utilisateur ne peuvent pas les exécuter. Parmi les instructions privilégiées on peut distinguer un sous-groupe appelé les '''instructions systèmes'''. Le premier type regroupe les '''instructions d'accès aux entrées-sorties''', aussi appelées instructions sensibles à la configuration. Le second type est celui des '''instructions de configuration du processeur''', qui agissent sur les registres de contrôle du processeur, aussi appelées instructions sensibles au comportement. Elles servent notamment à gérer la mémoire virtuelle, mais pas que. La théorie de Popek et Goldberg dit qu'il est possible de virtualiser un O.S à une condition : que les instructions systèmes soient toutes des instructions privilégiées, c’est-à-dire exécutables seulement en mode noyau. Virtualiser un O.S demande simplement de le démarrer en mode utilisateur. Quand l'O.S fait un accès au matériel, il le fait via une instruction privilégiée. Vu que l'OS est en mode utilisateur, cela déclenche une exception matérielle, qui émule l'instruction privilégiée. L'hyperviseur n'est ni plus ni moins qu'un ensemble de routines d'interruptions, chaque routine simulant le fonctionnement du matériel émulé. Par exemple, un accès au disque dur sera émulé par une routine d'interruption, qui utilisera les appels systèmes fournit par l'OS pour accéder au disque dur réellement présent dans l'ordinateur. Cette méthode est souvent appelée la méthode ''trap and emulate''. [[File:Virtualisation avec la méthode trap-and-emulate.png|centre|vignette|upright=2.0|Virtualisation avec la méthode trap-and-emulate]] La méthode ''trap and emulate'' ne fonctionne que si certaines contraintes sont respectées. Un premier problème est que beaucoup de jeux d'instructions anciens ne respectent pas la règle "les instructions systèmes sont toutes privilégiées". Par exemple, ce n'est pas le cas sur les processeurs x86 32 bits. Sur ces CPU, les instructions qui manipulent les drapeaux d'interruption ne sont pas toutes des instructions privilégiées, idem pour les instructions qui manipulent les registres de segmentation, celles liées aux ''call gates'', etc. A cause de cela, il est impossible d'utiliser la méthode du ''trap and emulate''. La seule solution qui ne requiert pas de techniques matérielles est de traduire à la volée les instructions systèmes problématiques en appels systèmes équivalents, grâce à des techniques de '''réécriture de code'''. Enfin, certaines instructions dites '''sensibles au contexte''' ont un comportement différent entre le mode noyau et le mode utilisateur. En présence de telles instructions, la méthode ''trap and emulate'' ne fonctionne tout simplement pas. Grâce à ces instructions, le système d’exploitation ou un programme applicatif peut savoir s'il s'exécute en mode utilisateur ou noyau, ou hyperviseur, ou autre. La virtualisation impose l'usage de la mémoire virtuelle, sans quoi plusieurs OS ne peuvent pas se partager la même mémoire physique. De plus, il ne faut pas que la mémoire physique, non-virtuelle, puisse être adressée directement. Et cette contrainte est violée, par exemple sur les architectures MIPS qui exposent des portions de la mémoire physique dans certaines zones fixées à l'avance de la mémoire virtuelle. L'OS est compilé pour utiliser ces zones de mémoire pour accéder aux entrées-sorties mappées en mémoire, entre autres. En théorie, on peut passer outre le problème en marquant ces zones de mémoire comme inaccessibles, toute lecture/écriture à ces adresses déclenche alors une exception traitée par l'hyperviseur. Mais le cout en performance est alors trop important. Quelques hyperviseurs ont été conçus pour les architectures MIPS, dont le projet de recherche DISCO, mais ils ne fonctionnaient qu'avec des systèmes d'exploitation recompilés, de manière à passer outre ce problème. Les OS étaient recompilés afin de ne pas utiliser les zones mémoire problématiques. De plus, les OS étaient modifiés pour améliorer les performances en virtualisation. Les OS disposaient notamment d'appels systèmes spéciaux, appelés des ''hypercalls'', qui exécutaient des routines de l'hyperviseur directement. Les appels systèmes faisant appel à des instructions systèmes étaient ainsi remplacés par des appels système appelant directement l'hyperviseur. Le fait de modifier l'OS pour qu'il communique avec un hyperviseur, dont il a connaissance de l'existence, s'appelle la '''para-virtualisation'''. [[File:Virtualization - Para vs Full.png|centre|vignette|upright=2.5|Virtualization - Para vs Full]] ==La virtualisation du processeur== La virtualisation demande de partager le matériel entre plusieurs machines virtuelles. Précisément, il faut partager : le processeur, la mémoire RAM, les entrées-sorties. Les trois sont gérés différemment. Par exemple, la virtualisation des entrées-sorties est gérée par l’hyperviseur, parfois aidé par le ''chipset'' de la carte mère. Virtualiser des entrées-sorties demande d'émuler du matériel inexistant, mais aussi de dupliquer des entrées-sorties de manière à ce le matériel existe dans chaque VM. Partager la mémoire RAM entre plusieurs VM est assez simple avec la mémoire virtuelle, bien que cela demande quelques adaptations. Maintenant, voyons ce qu'il en est pour le processeur. ===Le niveau de privilège hyperviseur=== Sur certains CPU modernes, il existe un niveau de privilège appelé le '''niveau de privilège hyperviseur''' qui est utilisé pour les techniques de virtualisation. Le niveau de privilège hyperviseur est réservé à l’hyperviseur et il a des droits d'accès spécifiques. Il n'est cependant pas toujours activé. Par exemple, si aucun hyperviseur n'est installé sur la machine, le processeur dispose seulement des niveaux de privilège noyau et utilisateur, le mode noyau n'ayant alors aucune limitation précise. Mais quand le niveau de privilège hyperviseur est activé, une partie des manipulations est bloquée en mode noyau et n'est possible qu'en mode hyperviseur. Le fonctionnement se base sur la différence entre instruction privilégiée et instruction système. Les instructions privilégiées peuvent s'exécuter en niveau noyau, alors que les instructions systèmes ne peuvent s'exécuter qu'en niveau hyperviseur. L'idée est que quand le noyau d'un OS exécute une instruction système, une exception matérielle est levée. L'exception bascule en mode hyperviseur et laisse la main à une routine de l'hyperviseur. L'hyperviseur fait alors des manipulations précise pour que l'instruction système donne le même résultat que si elle avait été exécutée par l'ordinateur simulé par la machine virtuelle. [[File:Virtualisation avec un mode hyperviseur.png|centre|vignette|upright=2|Virtualisation avec un mode hyperviseur.]] Il est ainsi possible d'émuler des entrées-sorties avec un cout en performance assez léger. Précisément, ce mode hyperviseur améliore les performances de la méthode du ''trap-and-emulate''. La méthode ''trap-and-emulate'' basique exécute une exception matérielle pour toute instruction privilégiée, qu'elle soit une instruction système ou non. Mais avec le niveau de privilège hyperviseur, seules les instructions systèmes déclenchent une exception, pas les instructions privilégiées non-système. Les performances sont donc un peu meilleures, pour un résultat identique. Après tout, les entrées-sorties et la configuration du processeur suffisent à émuler une machine virtuelle, les autres instructions noyau ne le sont pas. Sur les processeurs ARM, il est possible de configurer quelles instructions sont détournées vers le mode hyperviseur et celles qui restent en mode noyau. En clair, on peut configurer quelles sont les instructions systèmes et celles qui sont simplement privilégiées. Et il en est de même pour les interruptions : on peut configurer si elles exécutent la routine de l'OS normal en mode noyau, ou si elles déclenchent une exception matérielle qui redirige vers une routine de l’hyperviseur. En l'absence d'hyperviseur, toutes les interruptions redirigent vers la routine de l'OS normale, vers le mode noyau. Il faut noter que le mode hyperviseur n'est compatible qu'avec les hyperviseurs de type 1, à savoir ceux qui s'exécutent directement sur le matériel. Par contre, elle n'est pas compatible avec les hyperviseurs de type 2, qui sont des logiciels qui s'exécutent comme tout autre logiciel, au-dessus d'un système d'exploitation sous-jacent. ===L'Intel VT-X et l'AMD-V=== Les processeurs ARM de version v8 et plus incorporent un mode hyperviseur, mais pas les processeurs x86. À la place, ils incorporent des technologies alternatives nommées Intel VT-X ou l'AMD-V. Les deux ajoutent de nouvelles instructions pour gérer l'entrée et la sortie d'un mode réservé à l’hyperviseur. Mais ce mode réservé à l'hyperviseur n'est pas un niveau de privilège comme l'est le mode hyperviseur. L'Intel VT-X et l'AMD-V dupliquent le processeur en deux modes de fonctionnement : un mode racine pour l'hyperviseur, un mode non-racine pour l'OS et les applications. Fait important : les niveaux de privilège sont dupliqués eux aussi ! Par exemple, il y a un mode noyau racine et un mode noyau non-racine, idem pour le mode utilisateur, idem pour le mode système (pour le BIOS/UEFI). De même, les modes réel, protégé, v8086 ou autres, sont eux aussi dupliqués en un exemplaire racine et un exemplaire non-racine. L'avantage est que les systèmes d'exploitation virtualisés s'exécutent bel et bien en mode noyau natif, l'hyperviseur a à sa disposition un mode noyau séparé. D'ailleurs, les deux modes ont des registres d'interruption différents. Le mode racine et le mode non-racine ont chacun leurs espaces d'adressage séparés de 64 bit, avec leur propre table des pages. Et cela demande des adaptations au niveau de la TLB. La transition entre mode racine et non-racine se fait lorsque le processeur exécute une instruction système ou lors de certaines interruptions. Au minimum, toute exécution d'une instruction système fait commuter le processeur mode racine et lance l'exécution des routines de l’hyperviseur adéquates. Les interruptions matérielles et exceptions font aussi passer le CPU en mode racine, afin que l’hyperviseur puisse gérer le matériel. De plus, afin de gérer le partage de la mémoire entre OS, certains défauts de page déclenchent l'entrée en mode racine. Les ''hypercalls'' de la para-virtualisation sont supportés grâce à aux instructions ''vmcall'' et ''vmresume'' qui permettent respectivement d'appeler une routine de l’hyperviseur ou d'en sortir. La transition demande de sauvegarder/restaurer les registres du processeur, comme avec les interruptions. Mais cette sauvegarde est réalisée automatiquement par le processeur, elle n'est pas faite par les routines de l'hyperviseur. L’implémentation de cette sauvegarde/restauration se fait surtout via le microcode du processeur, car elle demande beaucoup d'étapes. Elle est en conséquence très lente. Le processeur sauvegarde l'état de chaque machine virtuelle en mémoire RAM, dans une structure de données appelée la ''Virtual Machine Control Structure'' (VMCS). Elle mémorise surtout les registres du processeur à l'instant t. Lorsque le processeur démarre l'exécution d'une VM sur le processeur, cette VMCS est recopiée dans les registres pour rétablir la VM à l'endroit où elle s'était arrêtée. Lorsque la VM est interrompue et doit laisser sa place à l'hyperviseur, les registres et l'état du processeur sont sauvegardés dans la VMCS adéquate. ==La virtualisation de la mémoire : mémoire virtuelle et MMU== Avec la virtualisation, les différentes machines virtuelles, les différents OS doivent se partager la mémoire physique, en plus d'être isolés les uns des autres. L'idée est d'utiliser la mémoire virtuelle pour cela. L'espace d'adressage physique vu par chaque OS est en réalité un espace d'adressage fictif, qui ne correspond pas à la mémoire physique. Les adresses physiques manipulées par l'OS sont en réalité des adresses intermédiaires entre les adresses physiques liées à la RAM, et les adresses virtuelles vues par les processus. Pour les distinguer, nous parlerons d'adresses physiques de l'hôte pour parler des adresses de la RAM, et des adresses physiques invitées pour parler des adresses manipulées par les OS virtualisés. Sans accélération matérielle, la traduction des adresses physiques invitées en adresses hôte est réalisée par une seconde table des pages, appelée la ''shadow page table'', ce qui donnerait '''table des pages cachée''' en français. La table des pages cachée est prise en charge par l'hyperviseur. Toute modification de la table des pages cachée est réalisée par l'hyperviseur, les OS ne savent même pas qu'elle existe. [[File:Shadowpagetables.png|centre|vignette|upright=2|Table des pages cachée.]] ===La MMU et la virtualisation : les tables des pages emboitées=== Une autre solution demande un support matériel des tables des pages emboitées, à savoir qu'il y a un arbre de table des pages, chaque consultation de la première table des pages renvoie vers une seconde, qui renvoie vers une troisième, et ainsi de suite jusqu'à tomber sur la table des pages finale qui renvoie l'adresse physique réelle. L'idée est l'utiliser une seule table des pages, mais d'ajouter un ou deux niveaux supplémentaires. Pour l'exemple, prenons le cas des processeurs x86. Sans virtualisation, l'OS utilise une table des pages de 4 niveaux. Avec, la table des pages a un niveau en plus, qui sont ajoutés à la fin de la dernière table des pages normale. Les niveaux ajoutés s'occupent de la traduction des adresses physiques invitées en adresses physiques hôte. On parle alors de '''table des pages étendues''' pour désigner ce nouveau format de table des pages conçu pour la virtualisation. Il faut que le processeur soit modifié de manière à parcourir automatiquement les niveaux ajoutés, ce qui demande quelques modifications de la TLB et du ''page table walker''. Les modifications en question ne font que modifier le format normal de la table des pages, et sont donc assez triviales. Elles ont été implémentées sur les processeurs AMD et Intel. AMD a introduit les tables des pages étendues sur ses processeurs Opteron, destinés aux serveurs, avec sa technologie ''Rapid Virtualization Indexing''. Intel, quant à lui, a introduit la technologie sur les processeurs i3, i5 et i7, sous le nom ''Extended Page Tables''. Les processeurs ARM ne sont pas en reste avec la technologie ''Stage-2 page-tables'', qui est utilisée en mode hyperviseur. ===La virtualisation de l'IO-MMU=== Si la MMU du processeur est modifiée pour gérer des tables des pages étendues, il en est de même pour les IO-MMU des périphériques et contrôleurs DMA.Les périphériques doivent idéalement intégrer une IO-MMU pour faciliter la virtualisation. La raison est globalement la même que pour le partage de la mémoire. Les pilotes de périphériques utilisent des adresses qui sont des adresses physiques sans virtualisation, mais qui deviennent des adresses virtuelles avec. Quand le pilote de périphérique configure un contrôleur DMA, pour transférer des données de la RAM vers un périphérique, il utilisera des adresses virtuelles qu'il croit physique pour adresser les données en RAM. Pour éviter tout problème, le contrôleur DMA doit traduire les adresses qu'il reçoit en adresses physiques. Pour cela, il y a besoin d'une IO-MMU intégrée au contrôleur DMA, qui est configurée par l'hyperviseur. Toute IO-MMU a sa propre table des pages et l'hyperviseur configure les table des pages pour chaque périphérique. Ainsi, le pilote de périphérique manipule des adresses virtuelles, qui sont traduites en adresses physiques directement par le matériel lui-même, sans intervention logicielle. Pour gérer la virtualisation, on fait la même chose qu'avec une table des pages emboitée habituelle : on l'étend en ajoutant des niveaux. L'IO-MMU peut fonctionner dans un mode normal, sans virtualisation, où les adresses virtuelles reçues du ''driver'' sont traduite avec une table des pages normale, non-emboitée. Mais elle a aussi un mode virtualisation qui utilise des tables de pages étendues. ==La virtualisation des entrées-sorties== Virtualiser les entrées-sorties est simple sur le principe. Un OS communique avec le matériel soit via des ports IO, soit avec des entrées-sorties mappées en mémoire. Le périphérique répond avec des interruptions ou via des transferts DMA. Virtualiser les périphériques demande alors d'émuler les ports IO, les entrées-sorties mappées en mémoire, le DMA et les interruptions. ===La virtualisation logicielle des interruptions=== Émuler les ports IO est assez simple, vu que l'OS lit ou écrit dedans grâce à des instructions IO spécialisées. Vu que ce sont des instructions système, la méthode ''trap and emulate'' suffit. Pour les entrées-sorties mappées en mémoire, l'hyperviseur a juste à marquer les adresses mémoires concernées comme étant réservées/non-allouées/autre. Tout accès à ces adresses lèvera une exception matérielle d'accès mémoire interdit, que l’hyperviseur intercepte et gère via ''trap and emulate''. L'émulation du DMA est triviale, vu que l'hyperviseur a accès direct à celui-ci, sans compter que l'usage d'une IO-MMU résout beaucoup de problèmes. La gestion des interruptions matérielles, les fameuses IRQ, est quant à elle plus complexe. Les interruptions matérielles ne sont pas à prendre en compte pour toutes les machines virtuelles. Par exemple, si une machine virtuelle n'a pas de carte graphique, pas besoin qu'elle prenne en compte les interruptions provenant de la carte graphique. La gestion des interruptions matérielles n'est pas la même si l'ordinateur grée des cartes virtuelles ou s'il se débrouille avec une carte physique unique. Lors d'une interruption matérielle, le processeur exécute la routine adéquate de l'hyperviseur. Celle-ci enregistre qu'il y a eu une IRQ et fait quelques traitements préliminaires. Ensuite, elle laisse la main au système d'exploitation concerné, qui exécute alors sa routine d'interruption. Une fois la routine de l'OS terminée, l'OS dit au contrôleur d'interruption qu'il a terminé son travail. Mais cela demande d'interagir avec le contrôleur d'interruption, ce qui déclenche une exception qui appelle l'hyperviseur. L'hyperviseur signale au contrôleur d'interruption que l'interruption matérielle a été traitée. Il rend alors définitivement la main au système d'exploitation. Le processus complet demande donc plusieurs changements entre mode hyperviseur et OS, ce qui est assez couteux en performances. Vu que le matériel simulé varie d'une machine virtuelle à l'autre, chaque machine virtuelle a son propre vecteur d'interruption. Par exemple, si une machine virtuelle n'a pas de carte graphique son vecteur d'interruption ne pointera pas vers les routines d'interruption d'un quelconque GPU. L'hyperviseur gère les différents vecteurs d'interruption de chaque VM et traduit les interruptions reçues en interruptions destinées aux VM/OS. Si la méthode ''trap and emulate'' fonctionne, ses performances ne sont cependant pas forcément au rendez-vous. Tous les matériels ne se prêtent pas tous bien à la virtualisation, surtout les périphériques anciens. Pour éliminer une partie de ces problèmes, il existe différentes techniques, accélérées en matériel ou non. Elles permettent aux machines virtuelles de communiquer directement avec les périphériques, sans passer par l'hyperviseur. ===La virtualisation des périphériques avec l'affectation directe=== Virtualiser les entrées-sorties avec de bonnes performances est plus complexe. En pratique, cela demande une intervention du matériel. Le ''chipset'' de la carte mère, les différents contrôleurs d'interruption et bien d'autres circuits doivent être modifiés. Diverses techniques permettent de faciliter le partage des entrées-sorties entre machines virtuelles. La première est l''''affectation directe''', qui alloue un périphérique à une machine virtuelle et pas aux autres. Par exemple, il est possible d'assigner la carte graphique à une machine virtuelle tournant sur Windows, mais les autres machines virtuelles ne verront même pas la carte graphique. Même l'hyperviseur n'a pas accès directement à ce matériel. L'affectation directe est très utile sur les serveurs, qui disposent souvent de plusieurs cartes réseaux et peuvent en assigner une à chaque machine virtuelle. Mais dans la plupart des cas, elle ne marche pas. De plus, sur les périphériques sans IO-MMU, elle ouvre la porte à des attaques DMA, où une machine virtuelle accède à la mémoire physique de la machine en configurant le contrôleur DMA de son périphérique assigné. L'affectation directe est certes limitée, mais elle se marie bien avec certaines de virtualisation matérielles, intégrées dans de nombreux périphériques. Il existe des périphériques qui sont capables de se virtualiser tout seuls, à savoir qu'ils peuvent se dédoubler en plusieurs '''périphériques virtuels'''. Par exemple, prenons une carte réseau avec cette propriété. Il n'y a qu'une seule carte réseau dans l'ordinateur, mais elle peut donner l'illusion qu'il y en a 8-16 d'installés dans l'ordinateur. Il faut alors faire la différence entre la carte réseau physique et les 8-16 cartes réseau virtuelles. L'idée est d'utiliser l'affectation directe, chaque machine virtuelle/OS ayant une carte réseau virtuelle d'affectée, avec affectation directe. [[File:Virtualisation matérielle des périphériques.png|centre|vignette|upright=2|Virtualisation matérielle des périphériques]] Pour les périphériques PCI-Express, le fait de se dupliquer en plusieurs périphériques virtuels est permis par la technologie '''''Single-root input/output virtualization''''', abrévié en SRIOV. Elle est beaucoup, utilisée sur les cartes réseaux, pour plusieurs raisons. Déjà, ce sont des périphériques beaucoup utilisés sur les serveurs, qui utilisent beaucoup la virtualisation. Dupliquer des cartes réseaux et utiliser l'affectation directe rend la configuration des serveurs bien plus simple. De plus, la plupart des cartes réseaux sont sous-utilisées, même par les serveurs. Une carte réseau est souvent utilisée à environ 10% de ses capacités par une VM unique, ce qui fait qu'utiliser 10 cartes réseaux virtuelles permet d'utiliser les capacités de la carte réseau à 100%. Il est possible de faire une analogie entre les processeurs multithreadés et les périphériques virtuels. Un processeur multithreadé est dupliqué en plusieurs processeurs virtuels, un périphérique virtualisé est dupliqué en plusieurs périphériques virtuels. L'implémentation des deux techniques est similaire sur le principe, mais les détails varient selon qu'on parle d'une carte réseau, d'une carte graphique, d'une carte son, etc. Pour gérer plusieurs périphériques virtuels, le périphérique physique contient plusieurs copies de ses registres de commande/données, plusieurs files de commandes, etc. De plus, le périphérique physique contient divers circuits d'arbitrage, qui gèrent comment le matériel est utilisé. Ils donnent accès à tour de rôle à chaque VM aux ressources non-dupliquées. [[File:Implémentation d'une carte réseau gérant plusieurs cartes réseaux virtuelles.png|centre|vignette|upright=2|Implémentation d'une carte réseau gérant plusieurs cartes réseaux virtuelles]] Dans le cas le plus simple, le matériel traite les commandes provenant des différentes VM dans l'ordre d'arrivée, une par une, il n'y a pas d'arbitrage pour éviter qu'une VM monopolise le matériel. Plus évolué, le matériel peut faire de l'affectation au tour par tour, en traitant chaque VM dans l'ordre durant un certain temps. Le matériel peut aussi utiliser des algorithmes d'ordonnancement/répartition plus complexes. Par exemple, les cartes graphiques modernes utilisent des algorithmes de répartition/ordonnancement accélérés en matériel, implémentés dans le GPU lui-même. ===La virtualisation des interruptions=== La gestion des interruptions matérielles peut aussi être accélérée en matériel, en complément des techniques de périphériques virtuels vues plus haut. Par exemple, il est possible de gérer des ''exitless interrupts'', qui ne passent pas du tout par l'hyperviseur. Mais cela demande d'utiliser l'affectation directe, en complément de l'usage de périphériques virtuels. Tout périphérique virtuel émet des interruptions distinctes des autres périphérique virtuel. Pour distinguer les interruptions provenant de cartes virtuelles de celles provenant de cartes physiques, on les désigne sous le terme d''''interruptions virtuelles'''. Une interruption virtuelle est destinée à une seule machine virtuelle : celle à laquelle est assignée la carte virtuelle. Les autres machines virtuelles ne reçoivent pas ces interruptions. Les interruptions virtuelles ne sont pas traitées par l'hyperviseur, seulement par l'OS de la machine virtuelle assignée. Une subtilité a lieu sur les processeurs à plusieurs cœurs. Il est possible d'assigner un cœur à chaque machine virtuelle, possiblement plusieurs. Par exemple, un processeur octo-coeur peut exécuter 8 machines virtuelles simultanément. Avec l'affectation directe ou à tour de rôle, l'interruption matérielle est donc destinée à une machine virtuelle, donc à un cœur. L'IRQ doit donc être redirigée vers un cœur bien précis et ne pas être envoyée aux autres. Les contrôleurs d'interruption modernes déterminent à quelles machines virtuelles sont destinées telle ou telle interruption, et peuvent leur envoyer directement, sans passer par l'hyperviseur. Grâce à cela, l'affectation directe à tour de rôle ont de bonnes performances. En plus de ce support des interruptions virtuelles, le contrôleur d'interruption peut aussi être virtualisé, à savoir être dupliqué en plusieurs '''contrôleurs d'interruption virtuels'''. Sur les systèmes à processeur x86, le contrôleur d'interruption virtualisé est l'APIC (''Advanced Programmable Interrupt Controller''). Diverses technologies de vAPIC, aussi dites d'APIC virtualisé, permettent à chaque machine virtuelle d'avoir une copie virtuelle de l'APIC. Pour ce faire, tous les registres de l'APIC sont dupliqués en autant d'exemplaires que d'APIC virtuels supportés. Il existe un équivalent sur les processeurs ARM, où le contrôleur d'interruption est nommé le ''Generic Interrupt Controller'' (GIC) et peut aussi être virtualisé. La virtualisation de l'APIC permet d'éviter d'avoir à passer par l'hyperviseur pour gérer les interruptions. Par exemple, quand un OS veut prévenir qu'il a fini de traiter une interruption, il doit communiquer avec le contrôleur d'interruption. Sans virtualisation du contrôleur d'interruption, cela demande de passer par l'intermédiaire de l'hyperviseur. Mais s'il est virtualisé, l'OS peut communiquer directement avec le contrôleur d'interruption virtuel qui lui est associé, sans que l'hyperviseur n'ait à faire quoique ce soit. De plus, la virtualisation du contrôleur d'interruption permet de gérer des interruptions inter-processeurs dites postées, qui ne font pas appel à l'hyperviseur, ainsi que des interruptions virtuelles émises par les IO-MMU. Sur les plateformes ARM, les ''timers'' sont aussi virtualisés. ==Annexe : le mode virtuel 8086 des premiers CPU Intel== Les premiers processeurs x86 étaient rudimentaires. Le 8086 utilisait une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, dont le but était d'adresser plus de 64 kibioctets de mémoire avec un processeur 16 bits. Sur le processeur 286, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'espace d'adressage en mode protégé est passé de 24 bits sur le CPU 286, à 32 bits sur le 386 et les CPU suivants. Pour bien faire la différence, la segmentation du 8086 fut appelée le mode réel, et la nouvelle forme de segmentation fut appelée le mode protégé. Les programmes conçus pour le mode réel ne pouvaient pas s'exécuter en mode protégé. En clair, tous les programmes conçus pour le 8086 devaient fonctionner en mode réel, qui était supporté sur le 286 et les processeurs suivant. Pour corriger les problèmes observés sur le 286, le 386 a ajouté un '''mode 8086 virtuel''', une technique de virtualisation qui permet à des programmes de s'exécuter en mode réel dans une machine virtuelle dédiée appelée la '''VM V86'''. Notez que nous utiliserons l'abréviation V86 pour parler du mode virtuel 8086, ainsi que de tout ce qui est lié à ce mode. ===La virtualisation du DOS et la mémoire étendue=== Utiliser le mode V86 demande d'avoir un programme 8086 à lancer, mais aussi d'utiliser un '''hyperviseur V86'''. L’hyperviseur V86 est un véritable hyperviseur, qui s’exécute en mode noyau, exécute des routines d'interruption, gère les exceptions matérielles, etc. Il réside en mémoire dans une zone non-adressable en mode réel, mais accessible en mode protégé, celui-ci permettant d'adresser plus de RAM. L'hyperviseur est forcément exécuté en mode protégé. Le système d'exploitation DOS s'exécutait en mode réel, ce qui fait qu'il pouvait être émulé par le mode V86. Il était ainsi possible de lancer une ou plusieurs sessions DOS à partir d'un système d'exploitation multitâche comme Windows. Windows. Beaucoup de personnes nées avant les années 2000 ont sans doute profité de cette possibilité pour lancer des applications DOS sous Windows. Les applications étaient en réalité lancées dans une machine virtuelle grâce au mode V86. Windows implémentait un hyperviseur V86 de type 2, à savoir que c'était un logiciel qui s'exécutait sur un OS sous-jacent, ici [[File:Hyperviseur.svg|centre|vignette|upright=2|Hyperviseur]] Les applications DOS dans une VM V86 ne peuvent pas adresser plus d'un mébioctet de mémoire. L'ordinateur peut cependant avoir plus de mémoire RAM, notamment pour gérer l'hyperviseur V86. Diverses techniques permettaient aux applications DOS d'utiliser la mémoire au-delà du mébioctet, appelée la '''mémoire étendue'''. Les logiciels DOS accédaient à la mémoire étendue en passant par un intermédiaire logiciel, qui lui-même communiquait avec l'hyperviseur V86. L'intermédiaire est appelé le ''Extended Memory Manager'' (EMM), et il est concrètement implémenté par un driver sur DOS (HIMEM.SYS). Les applications DOS ne pouvaient pas adresser la mémoire étendue, mais pouvaient échanger des données avec l'EMM. Les logiciels peuvent ainsi déplacer des données dans la mémoire étendue pour les garder au chaud, puis les rapatrier dans la mémoire conventionnelle quand ils en avaient besoin. L'intermédiaire ''Extended Memory Manager'' s'occupe d’échanger des données entre mémoire conventionnelle et mémoire étendue. Pour cela, il switche entre mode réel et protégé à la demande, quand il doit lire ou écrire en mémoire étendue. Il ne faut pas confondre mémoire étendue et ''expanded memory''. Pour rappel, l'''expanded memory'' est un système de commutation de banque, qui autorise un va-et-vient entre une carte d'extension et une page de 64 kibioctets mappé en mémoire haute. Elle fonctionne sans mode protégé, sans virtualisation, sans mode V86. La mémoire étendue ne gère pas de commutation de banque et demande que la RAM en plus soit installée dans l'ordinateur, pas sur une carte d'extension. Par contre, il est possible d'émuler l'''expanded memory'' sans carte d'extension, en utilisant la mémoire étendue. Quelques ''chipsets'' de carte mère intégraient des techniques cela. Une émulation logicielle était aussi possible. L'émulation logicielle se basait sur une réécriture de l'interruption 67h utilisée pour adresser la technologie ''expanded memory''. L'hyperviseur V86 pouvait s'en charger, il avait juste à réécrire son allocateur de mémoire pour gérer cette interruption et quelques autres détails. ===Le fonctionnement du mode virtuel 8086 de base=== En mode V86, la segmentation du mode protégé est désactivée, seule la segmentation du mode réel est utilisée. Il y a quelques subtilités liées à la ligne A20 du bus d'adresse, déjà abordées auparavant dans ce cours. Sur les CPU 286 et ultérieurs, le processeur peut adresser 1 mébioctet (2^20 adresses), plus 64 kibioctets qui ne sont pas adressables sur le 8086. Le tout permet donc d'adresser les adresses allant de 0 à 0x010FFEFH. Et ces adresses sont utilisées pour les programmes en mode réel. Les adresses au-delà de l'adresse 0x010FFEFH sont typiquement le lieu de résidence de l’hyperviseur en RAM. Par contre, la pagination peut être activée par l’hyperviseur, afin d’exécuter plusieurs logiciels en mode réel simultanément. La mémoire virtuelle par pagination peut aussi être utile si l'ordinateur a peu de mémoire RAM, pas assez pour faire tourner le logiciel : l'espace d'adressage vu par le logiciel est un espace virtuel de grande taille, ce qui permet de lancer le logiciel, au prix de performances dégradées. Enfin, la gestion des entrées-sorties mappées en mémoire est aussi simplifiée. De plus, cela permettait d'adresser plus de mémoire RAM grâce aux adresses plus longues du mode protégé. Le processeur est configuré en mode V86, le bit VM du registre d'état spécifique est mis à 1. Le processeur utilise ce bit lorsqu'une instruction utilise les registres de segments, afin de savoir comment calculer les adresses, le calcul n'étant pas le même en mode réel et en mode protégé. Quelques instructions machines dépendent aussi de la valeur de ce bit, mais cela est traité au décodage de l'instruction. Il faut noter que dans le mode 8086 virtuel, les programmes peuvent utiliser les registres ajoutés sur le 386 et ultérieurs. Par exemple, le 8086 n'a que 4 registres de segment, alors que le 286 en a 6. Les programmes en mode 8086 virtuel peuvent utiliser les deux registres de segment supplémentaires. Il en est de même pour d'autres registres ajoutés par le 286, comme des registres de contrôle, des registres de debug, et quelques autres. Il en est de même pour les instructions ajoutées par le 286, le 386 et ultérieur, qui sont exécutables en mode virtuel 8086. Et elles sont nombreuses. La compatibilité n'était pas parfaite, il y avait quelques petites différences entre ce mode V86 et le mode réel du 8086, idem avec le mode réel du 286. Mais la grande majorité des applications n'avait aucun problème. Les problèmes étaient concentrés sur quelques instructions précises, notamment celles avec un préfixe LOCK. ===Les ''Virtual-8086 mode extensions''=== A partir du processeur Pentium, les processeurs x86 ont introduit des optimisations du mode V86, afin de rendre la virtualisation plus rapide. L'ensemble de ces optimisations est regroupé sous le terme de '''''Virtual-8086 mode extensions''''', abrévié en VME. Les optimisations du VMA étaient, pour certaines, utiles au-delà de la virtualisation et étaient activables indépendamment du reste du VME. Le VME introduisait des optimisations quant au traitement des interruptions, à savoir la gestion des interruptions virtuelles. De plus, le VME modifie la gestion de l'''interrupt flag'' du registre d'état. Pour rappel, ce bit permet d'activer ou de désactiver les interruptions masquables. Modifier le bit ''interrupt flag'' permettait de désactiver les interruptions masquables ou au contraire de les activer. Il se trouve que ce bit était accesible par les programmes exécutés en mode réel, qui pouvaient en faire ce qu'ils voulaient. Le mode réel n'étant pas prévu pour la multi-programmation, ce n'était pas un problème. Mais en mode V86, toute modification de ce bit se répercute sur les autres VM en mode V86. Pour éviter les problèmes, le VME a ajouté de quoi virtualiser cet ''interrupt flag'', avec une copie par machine virtuelle V86. Chaque programme modifiait sa propre copie de l'''interrupt flag'' sans altérer celle des autres programmes exécutés en mode V86, et surtout sans déclencher une exception matérielle gérée par l'hyperviseur. Bien qu'elles aient été introduites sur les processeurs Pentium, elles n'ont réellement été rendues publiques qu'après la sortie des processeurs de microarchitecture P6. Avant d'être rendue publique, la documentation du VME était une annexe de la documentation officielle, la fameuse annexe H. Elle était mentionnée dans la documentation officielle, mais était indisponible au grand public, seules quelques entreprises sous NDA y avait accès. <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=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> qnag9hjn34lm9dskj3ehbtn7pubzpc8 Astrologie/Les différents facteurs astrologiques/Autres facteurs d'Interprétation 0 82605 745964 745956 2025-07-05T12:10:42Z Kad'Astres 30330 745964 wikitext text/x-wiki #[[/Les Décans/]] #[[/Les Symboles Sabians (degrés symboliques)/]] #[[/La Lune Noire/]] #[[/Chiron/]] #[[/Les Étoiles Fixes/]] #[[/Les Astéroïdes/]] [[Catégorie:Astrologie]] li3mhz5wwlzm185unqp81mtg8q1i31y Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86 0 82608 745979 2025-07-05T14:24:06Z Mewtow 31375 Page créée avec « Dans ce chapitre, nous allons étudier des processeurs que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. ==Un étude des microarchitectures superscalaires x86 d'Intel== Nous allons nous d'abord voir les processeurs Intel... » 745979 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des processeurs que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. ==Un étude des microarchitectures superscalaires x86 d'Intel== Nous allons nous d'abord voir les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ap5gayftz14lekftqslt3nsha4fa6n2 745981 745979 2025-07-05T14:24:33Z Mewtow 31375 /* La microarchitecture Netburst du Pentium 4 */ 745981 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des processeurs que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. ==Un étude des microarchitectures superscalaires x86 d'Intel== Nous allons nous d'abord voir les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] b3lktn44fd50lw91dcjyx1fj2ceu653 745983 745981 2025-07-05T14:24:54Z Mewtow 31375 745983 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des processeurs que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. ==Une étude des microarchitectures superscalaires x86 d'Intel== Nous allons nous d'abord voir les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] 40ht04prgnbgypcuwl8sfonilx005qv 745984 745983 2025-07-05T14:25:36Z Mewtow 31375 /* Les microarchitectures ZEN d'AMD */ 745984 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des processeurs que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. ==Une étude des microarchitectures superscalaires x86 d'Intel== Nous allons nous d'abord voir les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <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=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> gadnfokgsrqimm1q1lpcq0hc06zyypb 745994 745984 2025-07-05T14:56:47Z Mewtow 31375 745994 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <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=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> jencnzjw9hmj6nnee2gz2v2p8dfpj2v 746002 745994 2025-07-05T14:59:17Z Mewtow 31375 /* Les microarchitectures ZEN d'AMD */ 746002 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 12tgslcbktgsesdy421p3wj5g91al64 746004 746002 2025-07-05T15:00:43Z Mewtow 31375 A 746004 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> a7y56p5xcr4w78cwavb9z1cyma6bgaf 746005 746004 2025-07-05T15:00:58Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746005 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 6is3igmjrszuq2vmh8b3xsibur68ohj 746009 746005 2025-07-05T15:16:22Z Mewtow 31375 /* Les microarchitectures ZEN d'AMD */ 746009 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> be1k6rpk9cushpn23ntcnliv6u3x8y2 746010 746009 2025-07-05T15:22:46Z Mewtow 31375 /* Les microarchitectures ZEN d'AMD */ 746010 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 0bs7tzoy2gw599lgipgh7fkcsmdnbvn 746011 746010 2025-07-05T15:23:53Z Mewtow 31375 /* Les microarchitectures ZEN d'AMD */ 746011 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> kg9eoean1byfd963dfl943ai3v9b8cd 746012 746011 2025-07-05T15:28:24Z Mewtow 31375 /* Les processeurs x86 superscalaires sans exécution dans le désordre */ 746012 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : l'Atom, un processeur basse consommation. Plus précisément, le tout premier processeur de cette gamme n'a pas d'exécution dans le désordre, les suivants l'ayant intégré. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup. L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 8ep7jgrboagdjz3gv675um8de7zeaix 746013 746012 2025-07-05T15:30:34Z Mewtow 31375 A 746013 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> li7hli38wq12xvilbu956os9r8z6xpq 746014 746013 2025-07-05T15:37:38Z Mewtow 31375 746014 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. Mais surtout, une source d'optimisation importante est la présence d'instructions ''load-up'', qui lisent un opérande en mémoire. 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 retardent ce décodage assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 9n87xey21ajrzp00utd5wh57kr87ehl 746015 746014 2025-07-05T15:38:26Z Mewtow 31375 746015 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. Mais surtout, une source d'optimisation importante est la présence d'instructions ''load-up'', qui lisent un opérande en mémoire. 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 retardent ce décodage assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 8vp1p8tkk753nelry9ofs0cc258k6h5 746016 746015 2025-07-05T15:41:04Z Mewtow 31375 746016 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ca implique. 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é. De plus, 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. A 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. Mais surtout, une source d'optimisation importante est la présence d'instructions ''load-up'', qui lisent un opérande en mémoire. 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 retardent ce décodage assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 8z58puy0ztkdrtvqqrd6ldgzfejooqp 746017 746016 2025-07-05T15:45:11Z Mewtow 31375 /* Le jeu d'instruction x86 pose des problèmes pour la superscalarité */ 746017 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> bbkr1oe5s7mykni1vgeu3wuhqor3ule 746018 746017 2025-07-05T15:59:13Z Mewtow 31375 /* Le Pentium 1/MMX et les pipelines U/V */ 746018 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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. 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 disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> lb0acy6gxgnsrlx99f2ux8hib6iuzmk 746019 746018 2025-07-05T15:59:33Z Mewtow 31375 /* Le Pentium 1/MMX et les pipelines U/V */ 746019 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> dufu1wwxz019afn5jlbe1i9fcndsfdm 746020 746019 2025-07-05T16:03:31Z Mewtow 31375 /* Les processeurs Atom d'Intel, de microarchitecture Bonnell */ 746020 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> spxrm4tk6cc1e1g6sgv7pprsrjjy2u3 746021 746020 2025-07-05T16:04:14Z Mewtow 31375 /* Les processeurs Atom d'Intel, de microarchitecture Bonnell */ 746021 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> lpwichsl3cvya3x0s1dg810nvkjxyn4 746022 746021 2025-07-05T16:06:52Z Mewtow 31375 /* Les processeurs Atom d'Intel, de microarchitecture Bonnell */ 746022 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 67ih5hn85r0eacq9x7wjqnn6sf57ht7 746023 746022 2025-07-05T16:07:27Z Mewtow 31375 /* Les processeurs x86 superscalaires sans exécution dans le désordre */ 746023 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> e7j497glwfz5bilryv0ududo187dhre 746024 746023 2025-07-05T16:07:36Z Mewtow 31375 /* Les processeurs x86 superscalaires sans exécution dans le désordre */ 746024 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> gdgea1m6hgfefhr1gfll56y01iqzcq4 746025 746024 2025-07-05T16:14:49Z Mewtow 31375 /* Les processeurs Atom d'Intel, de microarchitecture Bonnell */ 746025 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> luny79mlvf993bomjlq43spssco52fr 746027 746025 2025-07-05T16:30:14Z Mewtow 31375 /* Les processeurs Atom d'Intel, de microarchitecture Bonnell */ 746027 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 8ohgweyty72xvvsnkf34blyxy6yw9rx 746031 746027 2025-07-05T18:37:25Z Mewtow 31375 /* Les processeurs superscalaires Intel */ 746031 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Les premiers Pentium 2 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 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Elle a été utilisée sur les processeurs Core 1 et 2, dont les Core 2 Duo. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 5jckri60gxvf717ahl7xv68z23dpjxd 746032 746031 2025-07-05T18:44:02Z Mewtow 31375 /* Les processeurs superscalaires Intel */ 746032 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les microarchitectures ont suivi un motif assez simple, appelé modèle '''tic-toc'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Kaby Lake, et ainsi de suite. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> musubg4827ojqn3he283djbh4fv3p5g 746033 746032 2025-07-05T18:45:20Z Mewtow 31375 /* Les processeurs superscalaires Intel */ 746033 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> n21ban3pq05hz20gqlqdqptp6ghml6b 746034 746033 2025-07-05T18:47:32Z Mewtow 31375 /* Les processeurs superscalaires Intel */ 746034 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 94agqumasw2n67vumoc7p4qorkkai5y 746035 746034 2025-07-05T18:55:09Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746035 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 6n4snce9z77bf202pagpge8olk3khnv 746036 746035 2025-07-05T18:56:36Z Mewtow 31375 /* La microarchitecture Netburst du Pentium 4 */ 746036 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ====La microarchitecture Core==== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> emk3r4641b69l1mmgxfed5fdy15gwck 746037 746036 2025-07-05T19:01:31Z Mewtow 31375 /* La microarchitecture Core */ 746037 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ====La microarchitecture Core==== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées du tampon de ré-ordonnancement et dans une fenêtre d'instruction unique de 32 micro-opérations. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 2c2dbukdhkbfnlbogihsws0uvpfunag 746038 746037 2025-07-05T19:07:03Z Mewtow 31375 /* La microarchitecture Core */ 746038 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ====La microarchitecture Core==== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les techniques de désambiguation mémoire sont implémentées sur cette micro-architecture. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> oseutpzv77mkij0q79zoah4b15r9tsi 746039 746038 2025-07-05T19:09:07Z Mewtow 31375 /* La microarchitecture Core */ 746039 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. Le pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. En parallèle, Intel a développé des processeurs basse consommation pour PC portables et applications embarquées. Il s'agit de la gamme des processeurs Atom. La première micro-architecture, Bonnell, a été vue plus haut. Elle était superscalaire, mais sans exécution dans le désordre. Elle a été suivie par la micro-architecture Silvermont, avec exécution dans le désordre, elle-même suivie par les micro-architectures Goldmont, Goldmont plus, puis Tremont, Gracemont, et Crestmont. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> e1b5a25mf4a13cdxak5evd0rkikx3td 746040 746039 2025-07-05T19:17:59Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746040 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias. [[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]] 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. 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. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Accès au tampon de ré-ordonnancement (''μop re-ordering buffer read'') ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 96if9uomitqhrl16t9bnl05s6vorvia 746041 746040 2025-07-05T19:23:29Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746041 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Accès au tampon de ré-ordonnancement (''μop re-ordering buffer read'') ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> r59fen1ti3mu4jfujy667rzqo8nwwd4 746042 746041 2025-07-05T19:28:45Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746042 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprendre en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3, mais le renommage de registre réutilise le banc de registre physique du Pentium 4. C'est un processeur quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> kxwgc64qijwr42x8k48mh55f6w631bf 746044 746042 2025-07-05T19:37:43Z Mewtow 31375 /* La microarchitecture Core */ 746044 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3. La microarchitecture passe à la quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3, la prédiction de branchement a été améliorée. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets, il y 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. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Un ''stack engine'' a été ajouté, de même que des techniques de micro-fusion et un ''Loop buffer''. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 7llcojk2fhc7ng2bsu4k331oyp95s5v 746045 746044 2025-07-05T19:43:08Z Mewtow 31375 /* La microarchitecture Core */ 746045 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. On retrouve la fenêtre d'instruction unique du Pentium 2/3. La microarchitecture passe à la quadruple émission, soit une instruction de plus que la triple émission des Pentium 2/3, la prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. Les instructions sont chargées depuis le cache d'instruction dans une mémoire tampon de 32 octets, il y 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. Les instructions sont alors prédécodées et insérées dans une file d'instruction de 18 entrées. Les instructions sont alors décodées par 4 décodeurs : trois décodeurs simples qui fournissent une seule micro-opération en sortie, et un décodeur complexe capable d’accéder au micro-code. En sortie, se trouve une file de micro-opérations de 7 entrées. Les micro-opérations sont alors renommées, puis envoyées à la fois dans un tampon de ré-ordonnancement de 96 entrées et dans une station de réservation unique de 32 micro-opérations. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] La microarchitecture Core supporte les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Elle ajoute aussi le support de la macro-fusion, qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Un ''stack engine'' a été ajouté, de même que des techniques de micro-fusion et un ''Loop Stream Detector''. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> lj7bn7j3tolgxw9b4v02ockrzl37xu4 746046 746045 2025-07-05T19:48:15Z Mewtow 31375 /* La microarchitecture Core */ 746046 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===La microarchitecture Core=== La microarchitecture Core fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. our le reste, les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> afdmleu8h95l2brfh6ptj3s9dgp5zch 746047 746046 2025-07-05T19:52:00Z Mewtow 31375 /* La microarchitecture Core */ 746047 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== La microarchitecture Core fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. our le reste, les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les deux sont reliés à une ''load store queue'', nommée ''memory ordering buffer'', lui-même relié au cache. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 8abmwadm3a7wftsvy64ai1yrutnahyy 746048 746047 2025-07-05T19:54:42Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746048 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> jt1yhne8ytiblgws8o4nne92jy37r1j 746049 746048 2025-07-05T19:57:58Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746049 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> d0bl0ctisbhi8yicm61yvzf1ngq2bmw 746050 746049 2025-07-05T20:03:45Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746050 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté, comme à chaque nouvelle micro-architecture. Pour le reste, rien ne change si ce n'est la prédiction de branchement. On reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> f4bsrp2wg1du1htowzna7ui29g6mww5 746051 746050 2025-07-05T20:08:21Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746051 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * La file d'instruction passe à 32 octets, il y 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> jsbwr32w63sqt9iq6n42czht97rwjht 746052 746051 2025-07-05T20:10:35Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746052 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. 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 décodeurs envoient les micro-opérations décodées dans une file, qui elle-même envoie les micro-opérations dans l'unité de renommage. Elle peut renommer 3 micro-opérations par cycle, avec 3 registres renommés par micro-opération. Les micro-opérations renommées sont ensuite envoyées au tampon de re-ordonnancement, le ROB. Les opérandes sont récupérées et copiées dans le ROB, si elles sont disponibles. Les opérandes disponibles dans le banc de registre sont lues depuis celui-ci. Vu que le banc de registre n'a que deux ports, ce qui n'est pas assez pour alimenter trois micro-opérations renommées, ce qui peut donner lieu à des délais. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 3p40gq535y2qaobpv0n2flbkfawj4un 746053 746052 2025-07-05T20:15:12Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746053 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. 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é, si l'instruction de destination n'est pas alignée sur 16 octets. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> elm122ub4whgt5yi7fb7csomcdw2v9d 746054 746053 2025-07-05T20:16:13Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746054 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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 pipeline faisait 14 à 12 étages, dont seuls les trois derniers correspondent au chemin de données. Voici le détail du pipeline : * 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 ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> ho8sq6bhuoyemwcdbjmqka9eekatjbb 746055 746054 2025-07-05T20:16:57Z Mewtow 31375 /* La microarchitecture P6 du Pentium 2/3 */ 746055 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, mais ajoutent un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. l'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 7f9os631n6xos8211gb7a34jgq8femh 746056 746055 2025-07-05T20:22:24Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746056 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * 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. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 7p85croddda6ft3rfp2kibmmgpsr8l2 746057 746056 2025-07-05T20:23:44Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746057 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 7cy1uxdvrjfnmsonbyw2q1lv31bbayv 746058 746057 2025-07-05T20:28:32Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746058 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. Les 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> 69u493j0x9s16admdprkymxee1hvbnp 746059 746058 2025-07-05T20:43:07Z Mewtow 31375 /* Les microarchitectures Core, Sandy Bridge and Ivy Bridge */ 746059 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. Les 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. De même, les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Le seule changement notable est le passage à un renommage dans le ROB à un renommage à banc de registre physique. Mais c'est aussi une modification qu'AMD a fait, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> fttz9t9bzpjl9b9akkm64rafe56cqhi 746060 746059 2025-07-05T20:44:43Z Mewtow 31375 /* Un étude des microarchitectures superscalaires x86 d'AMD */ 746060 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. Les 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. De même, les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Le seule changement notable est le passage à un renommage dans le ROB à un renommage à banc de registre physique. Mais c'est aussi une modification qu'AMD a fait, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> obfwtcfiwdpzyfohh2zwzp5sjdza0s3 746061 746060 2025-07-05T20:54:51Z Mewtow 31375 /* La microarchitecture Netburst du Pentium 4 */ 746061 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. ==Le jeu d'instruction x86 pose des problèmes pour la superscalarité== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A 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. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions ''load-up'' fait que beaucoup d'instructions sont décodées en deux micro-opérations. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions ''load-up''' assez loin dans le pipeline. En clair, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction ''load-up'' directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la '''micro-fusion''', sous-entendu de la fusion de deux micro-opérations. ==Les processeurs x86 superscalaires sans exécution dans le désordre== Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre. Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. Chose peu courante, 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 identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | colspan="2" | ALU simple (une par pipeline) |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps : * 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 ; * 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 NOP, qui ne fait rien. Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. 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 en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V. [[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.]] ===Les processeurs Atom d'Intel, de microarchitecture Bonnell=== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Les décodeurs sont assez différents de ceux observés sur les autres processeurs superscalaires. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==Les processeurs superscalaires Intel== Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Les processeurs d'AMD seront vus dans une section à part, à la suite de celle-ci. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée '''micro-architecture P5'''. Le Pentium 2 et le Pentium 3 utilisaient l''''architecture P6''' et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même. Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle 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é. La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les 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. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake. Le système tick-tock a alors été abandonné. [[File:IntelProcessorRoadmap-fr.svg|centre|vignette|upright=3|IntelProcessorRoadmap-fr]] ===La microarchitecture P6 du Pentium 2/3=== La microachitecture suivant le Pentium, nommée '''microarchitecture P6''', était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline : * 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 ; * Ecriture du résultat dans le ROB ; * Ecriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 2 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. ===Les microarchitectures Core, Sandy Bridge and Ivy Bridge=== Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées. La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'adder flottant. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour deux modifications majeures : le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. La taille des différentes structures, comme les stations de réservation ou le ROB, a aussi augmenté. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. La taille des stations de réservation, du ''Loop Stream Detector'' et du ROB, a augmenté, comme à chaque nouvelle micro-architecture, et les autres structures du processeur sont aussi dans ce cas. Pour le reste, rien ne change si ce n'est la prédiction de branchement et quelques détails mineurs. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. Les 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. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Une telle stagnation sur les unités de chargement et de décodage peut paraitre surprenante. Cependant, 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. Quand près de 80% des micro-opérations exécutées sont lues depuis ce cache, améliorer ce qu'il y a avant est peu utile, surtout au vu cu cout en circuit d'un décodeur supplémentaire. De même, les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Le seule changement notable est le passage à un renommage dans le ROB à un renommage à banc de registre physique. Mais c'est aussi une modification qu'AMD a fait, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. ===La microarchitecture Netburst du Pentium 4=== 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). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, l'impact des mauvaises prédictions était catastrophique. Pour compenser, l'unité de prédiction de branchement était une des plus évoluées pour l'époque. Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un, même chose pour le système de pipeline à ''replay''. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 a scindé la fenêtre d'instruction unique du Pentium 3 en deux : une file pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. 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]] Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées. [[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]] 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. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. [[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> t31xurb5lfpp1ld0rrn7aizb1gpr1hk Mathc complexes/069 0 82609 745989 2025-07-05T14:44:07Z Xhungab 23827 news 745989 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] : [[Mathc complexes/a26| '''Gauss-Jordan''']] : {{Partie{{{type|}}}|L'équation d'un polynôme}} : Copier la bibliothèque dans votre répertoire de travail avec les fichiers des parties précédentes : *[[Mathc complexes/062| '''d.h ..................... Déclaration des fichiers h''']] Ne pas conserver le fichier d.h avec la bibliothèque après avoir testé les exemples. '''Présentation :''' <syntaxhighlight lang="c"> Calculons les coefficients d'un polynôme. y = ax**2 + bx + c qui passe par ces trois points. x[1], y[1] x[2], y[2] x[3], y[3] En utilisant les points nous obtenons la matrice : x**2 x**1 x**0 y x[1]**2 x[1]**1 x[1]**0 y[1] x[2]**2 x[2]**1 x[2]**0 y[2] x[3]**2 x[3]**1 x[3]**0 y[3] Que nous pouvons écrire : x**2 x 1 y x[1]**2 x[1] 1 y[1] x[2]**2 x[2] 1 y[2] x[3]**2 x[3] 1 y[3] Utilisons la fonction gj_TP_mR(Ab); pour résoudre le système qui va nous donner les coefficients a,b,c </syntaxhighlight> Les exemples : * [[Mathc complexes/063|c01.c ]] ..... y = ax**2 + bx + c * [[Mathc complexes/064|c02.c ]] * [[Mathc complexes/065|c03.c ]] ..... y = ax**3 + bx**2 + cx + d * [[Mathc complexes/066|c04.c ]] * [[Mathc complexes/067|c05.c ]] ..... y = ax**4 + bx**3 + cx**2 + dx + e * [[Mathc complexes/068|c06.c ]] {{AutoCat}} 2wazrrte3hy8on89chfezxzoa12rxqz Mathc complexes/062 0 82610 745990 2025-07-05T14:46:09Z Xhungab 23827 news 745990 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] : '''Sommaire''' ◀ '''''Utilise la commande "Retour en Arrière" de ton navigateur.''' Installer ce fichier dans votre répertoire de travail. {{Fichier|d.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : d.h */ /* ------------------------------------ */ void i_A_b_with_XY_mZ( double **XY, double **A, double **b ) { int r = R1; int c = C1; int power = 0; for(r=R1; r<XY[R_SIZE][C0]; r++) { power=rsize_Z(XY)-R1; for(c=C1; c<A[C_SIZE][C0]; c++,c++) A[r][c]=pow(XY[r][C1],power--); b[r][C1]=XY[r][C3]; } } /* --------------------------------- */ void p_eq_poly_mZ( double **Ab ) { int r = R1; int power = rsize_Z(Ab)-R1; int cL = csize_Z(Ab)*C2-C1; printf(" y = "); for(r=R1;r<Ab[R_SIZE][C0];r++) if(Ab[r][cL]) { if(!power) printf(" %+.3f", Ab[r][cL]); else if(power==1){printf(" %+.3fx", Ab[r][cL]);power--;} else printf(" %+.3fx**%d",Ab[r][cL], power--); } printf("\n\n\n"); } /* --------------------------------- */ void verify_X_mZ( double **Ab, double x ) { int r = R1; int power = rsize_Z(Ab)-R1; int cL = csize_Z(Ab)*C2-C1; double y = 0.; for(;r<Ab[R_SIZE][C0];r++) y+= Ab[r][cL]*pow(x,power--); printf(" With x = %+.3f, y = %+.3f \n",x,y); } /* --------------------------------- */ /* --------------------------------- */ </syntaxhighlight> Déclaration des fichiers h. {{AutoCat}} 4q53fzjmk7h3ah2e8ux4ynoq35z4jcw Mathc complexes/063 0 82611 745991 2025-07-05T14:50:21Z Xhungab 23827 news 745991 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] 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 "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R3*(C2*C2)] ={ 1,0, 6,0, 2,0, 3,0, 3,0, 5,0 }; double **XY = ca_A_mZ(xy,i_mZ(R3,C2)); double **A = i_mZ(R3,C3); double **b = i_mZ(R3,C1); double **Ab = i_Abr_Ac_bc_mZ(R3,C3,C1); clrscrn(); printf("\n"); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**2 + bx + c (x**0 = 1) \n\n"); printf(" that passes through the points. \n\n"); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n Using the given points, we obtain this matrix\n\n"); printf(" x**2 x**1 x**0 y\n"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**2 + bx + c (x**0 = 1) that passes through the points. x y +1 +6 +2 +3 +3 +5 Using the given points, we obtain this matrix x**2 x**1 x**0 y +1.00 +1.00 +1.00 +6.00 +4.00 +2.00 +1.00 +3.00 +9.00 +3.00 +1.00 +5.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 +0.00 +0.00 +2.50 +0.00 +1.00 +0.00 -10.50 +0.00 +0.00 +1.00 +14.00 The coefficients a, b, c of the curve are : y = +2.500x**2 -10.500x +14.000 Press return to continue. x y +1 +6 +2 +3 +3 +5 Verify the result : With x = +1.000, y = +6.000 With x = +2.000, y = +3.000 With x = +3.000, y = +5.000 Press return to continue. </syntaxhighlight> {{AutoCat}} p2fzyr4kdhzg68wxkqr8ihmgpybotje Mathc complexes/064 0 82612 745992 2025-07-05T14:52:40Z Xhungab 23827 news 745992 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00b.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : c00b.c */ /* ------------------------------------ */ #include "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R3*(C2*C2)] ={ 1,0, -9,0, 2,0, 8,0, 3,0, -8,0 }; double **XY = ca_A_mZ(xy,i_mZ(R3,C2)); double **A = i_mZ(R3,C3); double **b = i_mZ(R3,C1); double **Ab = i_Abr_Ac_bc_mZ(R3,C3,C1); clrscrn(); printf("\n"); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**2 + bx + c (x**0 = 1) \n\n"); printf(" that passes through the points. \n\n"); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n Using the given points, we obtain this matrix\n\n"); printf(" x**2 x**1 x**0 y\n"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**2 + bx + c (x**0 = 1) that passes through the points. x y +1 -9 +2 +8 +3 -8 Using the given points, we obtain this matrix x**2 x**1 x**0 y +1.00 +1.00 +1.00 -9.00 +4.00 +2.00 +1.00 +8.00 +9.00 +3.00 +1.00 -8.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 +0.00 +0.00 -16.50 +0.00 +1.00 +0.00 +66.50 +0.00 +0.00 +1.00 -59.00 The coefficients a, b, c of the curve are : y = -16.500x**2 +66.500x -59.000 Press return to continue. x y +1 -9 +2 +8 +3 -8 Verify the result : With x = +1.000, y = -9.000 With x = +2.000, y = +8.000 With x = +3.000, y = -8.000 Press return to continue. </syntaxhighlight> {{AutoCat}} doqi7xfa4mkd23celpg7hp464l3d1ak Mathc complexes/065 0 82613 745993 2025-07-05T14:55:09Z Xhungab 23827 news 745993 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00c.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : c00c.c */ /* ------------------------------------ */ #include "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R4*(C2*C2)] ={ -5,0, -3,0, -2,0, 0,0, 2,0, 3,0, 3,0, -2,0 }; double **XY = ca_A_mZ(xy,i_mZ(R4,C2)); double **A = i_mZ(R4,C4); double **b = i_mZ(R4,C1); double **Ab = i_Abr_Ac_bc_mZ(R4,C4,C1); clrscrn(); printf("\n"); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**3 + bx**2 + cx + d \n\n"); printf(" that passes through the points. \n\n"); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n Using the given points, we obtain this matrix\n\n"); printf(" x**3 x**2 x**1 x**0 y\n"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); verify_X_mZ(Ab,XY[R4][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**3 + bx**2 + cx + d that passes through the points. x y -5 -3 -2 +0 +2 +3 +3 -2 Using the given points, we obtain this matrix x**3 x**2 x**1 x**0 y -125.00 +25.00 -5.00 +1.00 -3.00 -8.00 +4.00 -2.00 +1.00 +0.00 +8.00 +4.00 +2.00 +1.00 +3.00 +27.00 +9.00 +3.00 +1.00 -2.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 +0.00 +0.00 +0.00 -0.14 +0.00 +1.00 +0.00 +0.00 -0.73 +0.00 +0.00 +1.00 +0.00 +1.31 +0.00 +0.00 +0.00 +1.00 +4.43 The coefficients a, b, c of the curve are : y = -0.139x**3 -0.732x**2 +1.307x +4.429 Press return to continue. x y -5 -3 -2 +0 +2 +3 +3 -2 Verify the result : With x = -5.000, y = -3.000 With x = -2.000, y = +0.000 With x = +2.000, y = +3.000 With x = +3.000, y = -2.000 Press return to continue. </syntaxhighlight> {{AutoCat}} 3r00l6ap4yybxssuzjwgakoy47bvj2r Mathc complexes/066 0 82614 745995 2025-07-05T14:57:16Z Xhungab 23827 news 745995 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00d.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : c00d.c */ /* ------------------------------------ */ #include "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R4*(C2*C2)] ={ -5,0, -8,0, -2,0, 8,0, 2,0, -8,0, 5,0, 8,0}; double **XY = ca_A_mZ(xy,i_mZ(R4,C2)); double **A = i_mZ(R4,C4); double **b = i_mZ(R4,C1); double **Ab = i_Abr_Ac_bc_mZ(R4,C4,C1); clrscrn(); printf("\n"); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**3 + bx**2 + cx + d \n\n"); printf(" that passes through the points. \n\n"); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n Using the given points, we obtain this matrix\n\n"); printf(" x**3 x**2 x**1 x**0 y\n"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); verify_X_mZ(Ab,XY[R4][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**3 + bx**2 + cx + d that passes through the points. x y -5 -8 -2 +8 +2 -8 +5 +8 Using the given points, we obtain this matrix x**3 x**2 x**1 x**0 y -125.00 +25.00 -5.00 +1.00 -8.00 -8.00 +4.00 -2.00 +1.00 +8.00 +8.00 +4.00 +2.00 +1.00 -8.00 +125.00 +25.00 +5.00 +1.00 +8.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 +0.00 +0.00 +0.00 +0.27 +0.00 +1.00 +0.00 +0.00 +0.00 +0.00 +0.00 +1.00 +0.00 -5.07 +0.00 +0.00 +0.00 +1.00 +0.00 The coefficients a, b, c of the curve are : y = +0.267x**3 -5.067x**2 Press return to continue. x y -5 -8 -2 +8 +2 -8 +5 +8 Verify the result : With x = -5.000, y = -8.000 With x = -2.000, y = +8.000 With x = +2.000, y = -8.000 With x = +5.000, y = +8.000 Press return to continue. </syntaxhighlight> {{AutoCat}} a0yrwkmkikggpvlur9dwu9ywp9fa95z Mathc complexes/067 0 82615 746003 2025-07-05T14:59:19Z Xhungab 23827 news 746003 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00e.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : c00e.c */ /* ------------------------------------ */ #include "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R10*(C2*C2)] ={ 1,0, -5,0, 2,0, 8,0, 3,0, -7,0, 4,0, 1,0, 5,0, -4,0 }; double **XY = ca_A_mZ(xy,i_mZ(R5,C2)); double **A = i_mZ(R5,C5); double **b = i_mZ(R5,C1); double **Ab = i_Abr_Ac_bc_mZ(R5,C5,C1); clrscrn(); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**4 + bx**3 + cx**2 + dx + e \n\n"); printf(" that passes through the points. \n\n"); printf(" x y"); p_mRZ(XY,S5,P0,C6); printf(" Using the given points, we obtain this matrix\n\n"); printf(" x**4 x**3 x**2 x**1 x**0 y"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); verify_X_mZ(Ab,XY[R4][C1]); verify_X_mZ(Ab,XY[R5][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**4 + bx**3 + cx**2 + dx + e that passes through the points. x y +1 -5 +2 +8 +3 -7 +4 +1 +5 -4 Using the given points, we obtain this matrix x**4 x**3 x**2 x**1 x**0 y +1.00 +1.00 +1.00 +1.00 +1.00 -5.00 +16.00 +8.00 +4.00 +2.00 +1.00 +8.00 +81.00 +27.00 +9.00 +3.00 +1.00 -7.00 +256.00 +64.00 +16.00 +4.00 +1.00 +1.00 +625.00 +125.00 +25.00 +5.00 +1.00 -4.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 -0.00 +0.00 -0.00 +0.00 -3.63 +0.00 +1.00 -0.00 +0.00 +0.00 +44.75 +0.00 -0.00 +1.00 +0.00 +0.00 -191.88 +0.00 +0.00 -0.00 +1.00 +0.00 +329.75 +0.00 -0.00 +0.00 +0.00 +1.00 -184.00 The coefficients a, b, c of the curve are : y = -3.625x**4 +44.750x**3 -191.875x**2 +329.750x -184.000 Press return to continue. x y +1 -5 +2 +8 +3 -7 +4 +1 +5 -4 Verify the result : With x = +1.000, y = -5.000 With x = +2.000, y = +8.000 With x = +3.000, y = -7.000 With x = +4.000, y = +1.000 With x = +5.000, y = -4.000 Press return to continue. </syntaxhighlight> {{AutoCat}} qgfln6ctdkqbekvruq45qswizzv8ghe Mathc complexes/068 0 82616 746007 2025-07-05T15:01:38Z Xhungab 23827 news 746007 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/069| '''Application''']] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00f.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ------------------------------------ */ /* Save as : c00f.c */ /* ------------------------------------ */ #include "w_a.h" #include "d.h" /* --------------------------------- */ int main(void) { double xy[R10*(C2*C2)] ={ 1,0, -2,0, 2,0, -2,0, 3,0, 3,0, 4,0, -9,0, 5,0, 4,0 }; double **XY = ca_A_mZ(xy,i_mZ(R5,C2)); double **A = i_mZ(R5,C5); double **b = i_mZ(R5,C1); double **Ab = i_Abr_Ac_bc_mZ(R5,C5,C1); clrscrn(); printf(" Find the coefficients a, b, c of the curve \n\n"); printf(" y = ax**4 + bx**3 + cx**2 + dx + e \n\n"); printf(" that passes through the points. \n\n"); printf(" x y"); p_mRZ(XY,S5,P0,C6); printf(" Using the given points, we obtain this matrix\n\n"); printf(" x**4 x**3 x**2 x**1 x**0 y"); i_A_b_with_XY_mZ(XY,A,b); c_A_b_Ab_mZ(A,b,Ab); p_mRZ(Ab,S7,P2,C6); stop(); clrscrn(); printf(" The Gauss Jordan process will reduce this matrix to : \n"); gj_mZ(Ab); p_mRZ(Ab,S7,P2,C6); printf("\n The coefficients a, b, c of the curve are : \n\n"); p_eq_poly_mZ(Ab); stop(); clrscrn(); printf(" x y \n"); p_mRZ(XY,S5,P0,C6); printf("\n"); printf(" Verify the result : \n\n"); verify_X_mZ(Ab,XY[R1][C1]); verify_X_mZ(Ab,XY[R2][C1]); verify_X_mZ(Ab,XY[R3][C1]); verify_X_mZ(Ab,XY[R4][C1]); verify_X_mZ(Ab,XY[R5][C1]); printf("\n\n\n"); stop(); f_mZ(XY); f_mZ(A); f_mZ(b); f_mZ(Ab); return 0; } /* ------------------------------------ */ /* ------------------------------------ */ </syntaxhighlight> . '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Find the coefficients a, b, c of the curve y = ax**4 + bx**3 + cx**2 + dx + e that passes through the points. x y +1 -2 +2 -2 +3 +3 +4 -9 +5 +4 Using the given points, we obtain this matrix x**4 x**3 x**2 x**1 x**0 y +1.00 +1.00 +1.00 +1.00 +1.00 -2.00 +16.00 +8.00 +4.00 +2.00 +1.00 -2.00 +81.00 +27.00 +9.00 +3.00 +1.00 +3.00 +256.00 +64.00 +16.00 +4.00 +1.00 -9.00 +625.00 +125.00 +25.00 +5.00 +1.00 +4.00 Press return to continue. The Gauss Jordan process will reduce this matrix to : +1.00 -0.00 +0.00 -0.00 +0.00 +2.67 +0.00 +1.00 -0.00 +0.00 +0.00 -30.33 +0.00 -0.00 +1.00 +0.00 +0.00 +117.83 +0.00 +0.00 -0.00 +1.00 +0.00 -181.17 +0.00 -0.00 +0.00 +0.00 +1.00 +89.00 The coefficients a, b, c of the curve are : y = +2.667x**4 -30.333x**3 +117.833x**2 -181.167x +89.000 Press return to continue. x y +1 -2 +2 -2 +3 +3 +4 -9 +5 +4 Verify the result : With x = +1.000, y = -2.000 With x = +2.000, y = -2.000 With x = +3.000, y = +3.000 With x = +4.000, y = -9.000 With x = +5.000, y = +4.000 Press return to continue. </syntaxhighlight> {{AutoCat}} bwowler96aow09v14f1ec28f3tu0zo6