Aller au contenu

Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86

Un livre de Wikilivres.

Dans ce chapitre, nous allons étudier des exemples de processeurs superscalaires que vous avez peut-être eu dans un ancien ordinateur. Nous allons étudier les processeurs x86 des PC, et précisément les architectures à haute performances, avec de l'exécution dans le désordre, de la superscalarité, de la prédiction de branchement, et autres optimisations de ce genre. Précisément, tous les processeurs que nous allons voir maintenant sont des processeurs superscalaires. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires.

Le jeu d'instruction x86 pose des problèmes pour la superscalarité

[modifier | modifier le wikicode]

Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la superscalarité. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc.

Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité.

Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. A la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent.

Troisièmement, la présence d'instructions load-up, qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction load-op est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Rien de compliqué à cela, il faut juste tenir compte du fait que certaines instructions sont décodées en plusieurs micro-opérations. Sur les processeurs RISC, une instruction correspond globalement à une seule micro-opération, sauf éventuellement pour quelques instructions complexes. Mais sur les CPU CISC, la présence d'instructions load-up fait que beaucoup d'instructions sont décodées en deux micro-opérations.

Sauf que les processeurs x86 modernes optimisent la gestion des instructions load-up. Nous verrons que les premiers processeurs Atom géraient des micro-opérations de type load-up, directement dans le chemin de données ! D'autres processeurs retardent le décodage réel des instructions load-up' assez loin dans le pipeline. En clair, une instruction load-op est décodée en une seule "macro-opération", qui est une sorte de vraie/fausse micro-opération. Elle parcourt le pipeline jusqu'à arriver aux fenêtres d'instruction situées en amont des ALU et de l'unité mémoire. C'est là que la macro-opération est scindées en deux micro-opérations, exécutées l'une après l'autre.

L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. En comparaison, décoder une instruction load-up directement en deux micro-opérations en sortie du décodeurs utiliserait deux entrées. Intel comme AMD décrivent cette optimisation comme étant de la micro-fusion, sous-entendu de la fusion de deux micro-opérations.

Les processeurs x86 superscalaires sans exécution dans le désordre

[modifier | modifier le wikicode]

Pour commencer, nous allons voir deux cas de processeurs superscalaires qui ne gèrent pas l'exécution dans le désordre. L'apparition de la superscalarité s'est faite sur le processeur Intel Pentium 1, et a été conservée sur tous les processeurs Intel qui ont suivi. Le successeur du Pentium a intégré l'exécution dans le désordre, ce qui fait que Pentium le seul processeur superscalaire in-order. AMD n'a pas produit de processeurs in-order, tout les processeurs produits par AMD intégrent l'exécution dans le désordre.

Mais n'allez pas croire que nous n'allons voir que le Pentium dans cette section. En effet, un autre processeur est dans ce cas : les processeurs Atom première génération. Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. La toute première microarchitecture Atom était la microarchitecture Bonnell, une architecture superscalaire double émission, sans exécution dans le désordre. Les microarchitectures Atom suivante ont intégré l'exécution dans le désordre, ce qui fait qu'on n'en parlera pas dans cette section.

Le Pentium 1/MMX et les pipelines U/V

[modifier | modifier le wikicode]

Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante et qui est assez courante sur le processeurs superscalaires, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements.

Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et un barrel shifter pour les décalages/rotations. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.

Pipeline U Pipeline V
ALU entière ALU entière
Multiplieur/diviseur
Barrel Shifter
AGU complexe AGU simple (opération LEA)
FPU
Unité SIMD

Les deux ALU géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :

  • Les instructions arithmétiques INC, DEC, ADD, SUB ;
  • l'instruction de comparaison CMP ;
  • les instructions bit à bit AND, OR, XOR ;
  • l'instruction de calcul d'adresse LEA ;
  • l'instruction MOV (dépend du mode d'adressage) ;
  • les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ;
  • l'instruction NOP, qui ne fait rien.

Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.

Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.

Les processeurs Atom d'Intel, de microarchitecture Bonnell

[modifier | modifier le wikicode]

L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup.

Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.

Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions load-up. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances.

Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque.

Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par thread si le multithreading matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.

Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations.

Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction load-up nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un barrel shifter, et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières.

Intel Atom Microarchitecture

Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances.

Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.

Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important.

En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait éxecuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières.

Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée Safe Instruction Recognition par Intel, est décrite dans le brevet US00525721.6A.

L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat.

La microarchitecture Netburst du Pentium 4

[modifier | modifier le wikicode]

Nous venons de voir les processeurs Pentium et Atom (du moins les premiers processeurs Atom). Et intuitivement, vous vous dites qu'il est temps de passer au Pentium 2 et 3, puis aux autres processeurs Intel. Mais nous n'allons pas faire ça. A la place, nous allons voir l'architecture du processeur Pentium 4. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel.

Le Pentium 2 et 3 ont introduit l'exécution dans le désordre avec une station de réservation centralisée, et les autres processeurs Intel ont gardé celle-ci. Tous les processeurs Intel peuvent être vus comme de lointains cousins du Pentium 2/3. Les architectures Intel ont évolué dans le temps, en partant du Pentium 2/3 et en améliorant son design progressivement, avec des ajouts à chaque nouvelle micro-architecture. Il est donc intéressant de les voir dans l'ordre, en partant du Pentium 2/3. Mais le problème est que le Pentium 4 est totalement à part de cette continuité.

Le Pentium 4 a représenté une rupture en termes de microarchitecture. Elle n'avait plus rien à voir avec l'architecture des Pentium 2 et 3. Elle introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à replay n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. Concrétement, la microarchitecture du Pentium 4 a été un échec, ce qui fait que les processeurs suivants sont repartis sur la base du Pentium 3.

Un focus sur la fréquence d'horloge

[modifier | modifier le wikicode]

La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm).

Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures Willamette et Northwood avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures Prescott et Cedar Mill étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial.

Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles

Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un Branch Target Buffer de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante.

Le cache de trace du Pentium 4

[modifier | modifier le wikicode]

Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le cache de trace. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas.

Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions !

Cache de trace
Ligne de cache ADD SUB ADD MOV MUL shift
Ligne de cache ADD load-up MUL Branch if Equal
Ligne de cache XOR POP SUB
... ...

Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un bloc de base (basic block) est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une trace est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D.

Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée.

Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.

Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le program counter au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale.

Cache de traces.

Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction.

Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage.

L'exécution dans le désordre et le chemin de données du P4

[modifier | modifier le wikicode]

Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias.

Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes.

Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue.

Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le Store-to-load forwarding. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse.

Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit :

Ports d'émission du Pentium 4

Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique.

Microarchitecture du Pentium 4.

Les unités de calcul entières du Pentium 4

[modifier | modifier le wikicode]

Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU.

Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le barrel shifter. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne !

Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé addition étagée (staggered add) dans la documentation Intel.

Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc.

Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant.

Le replay pipeline

[modifier | modifier le wikicode]

Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un replay pipeline, dont nous avons déjà parlé dans ce cours.

Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées.

Les processeurs superscalaires Intel d'architecture P6-like

[modifier | modifier le wikicode]

Après avoir vu deux exemples de processeurs superscalaires sans exécution dans le désordre, nous allons aux processeurs avec exécution dans le désordre. Et nous allons commencer par les processeurs d'Intel. Un point important est que les microarchitectures d'Intel ont évolué au cours du temps. Et le moins qu'on puisse dire est qu'elles sont nombreuses, ce qui est assez normal quand on sait que le Pentium est sorti en 1993, soit il y a plusieurs décennies. Les micro-architectures que nous allons voir suivent celle du Pentium, appelée micro-architecture P5.

Le Pentium 2 et le Pentium 3 utilisaient l'architecture P6 et ses nombreuses variantes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement). Le pipeline passe de 5 étage sur le Pentium à 14 ! Elle a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas la même.

Le Pentium 4 a été un échec, il est rapidement apparu que cette microarchitecture était mal conçue et devait être remplacée. Pour cela, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. A partir de ce moment, les microarchitectures ont suivi un motif assez simple, appelé modèle tick-tock. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. La micro-architecture suivante reprenait la finesse de gravure de la précédente, dans sa seconde version. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove.

IntelProcessorRoadmap-fr

Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Le seul changement notable est le passage à un renommage dans le ROB à un renommage à banc de registre physique. Mais c'est aussi une modification qu'AMD a fait, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Un autre changement est l'introduction du cache de micro-opération, sur lequel nous reviendrons en détail.

Pour le reste, les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là.

La microarchitecture P6 du Pentium 2/3

[modifier | modifier le wikicode]

La microachitecture suivant le Pentium, nommée microarchitecture P6, était une microarchitecture plus élaborée. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation et un renommage de registre dans le ROB, commandé par une table d'alias. Le pipeline faisait 14 à 12 étages, dont le détail du pipeline :

  • Prédiction de branchement, deux cycles ;
  • Chargement des instructions, trois cycles ;
  • Décodage de l'instruction, deux cycles ;
  • Renommage de registre, un cycle ;
  • Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ;
  • Dispath dans ou depuis la station de réservation.
  • Exécution de l'instruction ;
  • Ecriture du résultat dans le ROB ;
  • Ecriture dans le banc de registre physique.

Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge.

Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge.

Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.

P6 func diag

Les premiers Pentium 2 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine.

Si on omet la parenthèse du Pentium 4, les microarchitectures Intel qui ont suivies se sont basées sur l'architecture P6 et l'ont améliorée graduellement. Il s'agit là d'un point important : il n'y a pas eu de grosse modifications pendant facilement une à deux décennies. Aussi, nous allons zapper le Pentium 4 pour poursuivre sur l'architecture Core et ses dérivées.

La microarchitecture Core

[modifier | modifier le wikicode]

La microarchitecture Core fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations load-up, avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures.

  • Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant.
  • La prédiction de branchement a été améliorée avec notamment l'ajout d'une Fetch Input Queue.
  • L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie.
  • Un stack engine et un Loop Stream Detector ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération.
  • Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture.

Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions.

La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant.

Pour les accès mémoire, le processeur contient deux unités de calcul d'adresse : une pour les écritures, une autre pour les lectures. Les deux sont reliées à une Load/Store Queue, appelée Memory Ordering Buffer. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture.

Intel Core microarchitecture

Les microarchitectures Sandy Bridge and Ivy Bridge

[modifier | modifier le wikicode]

Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un Loop Stream Detector).

L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline.

Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le legacy pipeline et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du legacy pipeline.

Le cache de micro-opérations est complété avec un Loop Stream Detector, placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le Loop Stream Detector utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres.

L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le Loop Stream Detector et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le Loop Stream Detector appliquant cette méthode au cache de micro-opération lui-même..

Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations.

Les microarchitectures Sandy Bridge and Ivy Bridge sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement

Les architectures Haswell et Broadwell ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent.

L'architecture Skylake réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc.

Les microarchitectures récentes d'Intel

[modifier | modifier le wikicode]

Les architectures Ice Lake et Tiger Lake passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après.

La microarchitecture Golden Cove, la plus récente, altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant.

Un étude des microarchitectures superscalaires x86 d'AMD

[modifier | modifier le wikicode]

Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble.

Étudier ces architectures demande de voir trois choses séparément : le front-end qui regroupe l'unité de chargement et les décodeurs, le back-end qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la Load Store Queue. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.

La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10

[modifier | modifier le wikicode]

La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d'AMD Athlon, les AMD K8 sont connus sous le nom d'AMD Athlon 64, et les architecture K10 sont appelées les AMD Phenom. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.

Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.

Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.

L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
Architecture AMD Caches
K5 L1 instruction L1 données
TLB unique
K6 L1 instruction L1 données L2 unifié
TLB L1 instruction TLB L1 données
K7, K8 L1 instruction L1 données L2 unifié
TLB L1 instruction TLB L1 données TLB L2 instruction TLB L2 données
K10 L1 instruction L1 données L2 unifié L3
TLB L1 instruction TLB L1 données TLB L2 instruction TLB L2 données

Fait important, les architectures K5 à K10 utilisent la technique du prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.

Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.

La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de Branch Target Buffer grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée.

Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.

Comparaison du chargement de l'AMD K5 et K6

Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.

L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.

Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions load-up.

La différence entre le K6 et le K7 s'explique par des optimisations des instructions load-up. Sur le K6, les instructions load-up sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction load-up est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.

Décodage sur le K5 et le K5

Les microarchitectures K5 et K6 d'AMD

[modifier | modifier le wikicode]

Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.

Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des sector caches.

Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.

Pour les unités entières, il y avait deux ALU simples, un barrel shifter et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le barrel shifter et le diviseur sont des ports différents.

AMD K5
Port X Port Y
ALU simple ALU simple
Barrel Shifter Diviseur

Sur l'architecture K6, le barrel shifter et le diviseur sont sur le même port.

AMD K6
Port X Port Y
ALU simple ALU simple
Barrel Shifter
Diviseur

Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la store queue n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.

Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.

Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.

Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.

L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.

AMDK5 Diagramme.

L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de padding inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.

Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de scratchpad que les datasheets d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.

AMD K6 original.

Les microarchitectures K7, K8 et K10 d'AMD

[modifier | modifier le wikicode]

Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.

L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.

A partir du K7, le CPU optimise la prise en charge des instructions load-up. Les instructions load-op sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions load-up sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions load-up sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.

Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'instruction control unit. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.

Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de Load-Store Queue, mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la Pre-Cache Queue. Si au vu de son nom, vous avez deviné qu'il y avait une Post-Cache Queue. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.

Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.

Unité de calcul des processeurs AMD K7, K8 et K10

La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.

Les stations de réservation sont nommées des schedulers dans les schémas qui suivent.

La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.

La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions load-up sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.

AMD K7

Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU.

AMD Husky microarchitecture

La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.

Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.

Et cela a un autre avantage : le renommage, dispatch et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.

Les microarchitectures ZEN d'AMD

[modifier | modifier le wikicode]

Viennent ensuite les microarchitectures Bulldozer, avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.

Les microarchitectures suivantes sont les architectures ZEN 1/2/3/4/5. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le branch target buffer augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du front-end, notamment pour les boucles.

La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées scheduler dans le schéma.

Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication.

Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations.

La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première.

Microarchitecture Zen 1 d'AMD.

Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse.

Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)