Aller au contenu

Fonctionnement d'un ordinateur/Les processeurs superscalaires

Un livre de Wikilivres.

Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les processeurs à émissions multiples.

Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.

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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

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.

Processeur sans émission multiple Chargement Décodage Renommage Émission Exécution / ALU Commit/ROB
Exécution / ALU
Processeur superscalaire Chargement Décodage Renommage Émission Exécution / ALU Commit/ROB
Exécution / ALU
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

[modifier | modifier le wikicode]

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.

Le séquenceur d'un processeur superscalaire

[modifier | modifier le wikicode]

Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.

L'étape de chargement superscalaire

[modifier | modifier le wikicode]

Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.

Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.

Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.

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.

Cache d'instructions autoaligné.

Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (merger). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.

Implémentation d'un cache d'instructions autoaligné.

Les décodeurs d'instructions superscalaires

[modifier | modifier le wikicode]

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é 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

[modifier | modifier le wikicode]

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.

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.

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.

Correction des dépendances sur un processeur superscalaire.

L'unité d'émission superscalaire

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

É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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

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.

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

[modifier | modifier le wikicode]

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.

Microarchitecture Zen 1 d'AMD.

L'émission multiple des micro-opérations entières

[modifier | modifier le wikicode]

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, parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.

Émission multiple des opérations entières, implémentation naïve.

Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.

Par exemple, on peut utiliser 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. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples, par exemple un décalage en même temps qu'une addition.

É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.

Emission multiple des opérations entières, implémentation courante

L'émission multiple des accès mémoire

[modifier | modifier le wikicode]

Après avoir vu l'émission multiple pour les opérations flottantes et entiè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. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.

Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.

L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.

Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.

La file de µops mémoire est appelée la Pre-Cache Queue dans les schéma qui suivent. La 'Post-Cache Queue est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.

Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.

AMD K7

Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.

AMD K8

Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.

Double émission avec le Load Ordering, Store Ordering

Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.

Les processeurs avec une Load-store queue ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.

Dans ce qui suit, nous parlerons d'AGU (Adress Generation Unit) pour désigner les unités de calcul d'adresse.
Emission multiple des µops mémoire.

Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.

D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.

L'interaction avec les fenêtres d'instruction

[modifier | modifier le wikicode]

Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.

Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).

  • Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
  • Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
  • Le troisième alimente une ALU entière, sans circuit flottant dédié.

Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.

Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !

Microarchitecture du PowerPC 440.

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

[modifier | modifier le wikicode]

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.

Pipeline Bypass

Les problèmes du contournement sur les CPU avec beaucoup d'ALUs

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.

Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée COPY.

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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

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.