Fonctionnement d'un ordinateur/Les processeurs superscalaires
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.

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.
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 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
[modifier | modifier le wikicode]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.

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.

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.

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
[modifier | modifier le wikicode]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.


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
[modifier | modifier le wikicode]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
[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.

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.

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.

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.

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.

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

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.

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.

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 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
[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 !

Résumé
[modifier | modifier le wikicode]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.

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

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.